From d83f886519c1e20d3eadfa03a575ed8d0022cdea Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Tue, 3 Apr 2018 16:36:43 +0200
Subject: [PATCH 001/380] migrated jquery.flot.events to ts

---
 .../plugins/panel/graph/jquery.flot.events.js | 604 ----------------
 .../plugins/panel/graph/jquery.flot.events.ts | 663 ++++++++++++++++++
 2 files changed, 663 insertions(+), 604 deletions(-)
 delete mode 100644 public/app/plugins/panel/graph/jquery.flot.events.js
 create mode 100644 public/app/plugins/panel/graph/jquery.flot.events.ts

diff --git a/public/app/plugins/panel/graph/jquery.flot.events.js b/public/app/plugins/panel/graph/jquery.flot.events.js
deleted file mode 100644
index 3ea3ca8f330..00000000000
--- a/public/app/plugins/panel/graph/jquery.flot.events.js
+++ /dev/null
@@ -1,604 +0,0 @@
-define([
-  'jquery',
-  'lodash',
-  'angular',
-  'tether-drop',
-],
-function ($, _, angular, Drop) {
-  'use strict';
-
-  function createAnnotationToolip(element, event, plot) {
-    var injector = angular.element(document).injector();
-    var content = document.createElement('div');
-    content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
-
-    injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
-      var eventManager = plot.getOptions().events.manager;
-      var tmpScope = $rootScope.$new(true);
-      tmpScope.event = event;
-      tmpScope.onEdit = function() {
-        eventManager.editEvent(event);
-      };
-
-      $compile(content)(tmpScope);
-      tmpScope.$digest();
-      tmpScope.$destroy();
-
-      var drop = new Drop({
-        target: element[0],
-        content: content,
-        position: "bottom center",
-        classes: 'drop-popover drop-popover--annotation',
-        openOn: 'hover',
-        hoverCloseDelay: 200,
-        tetherOptions: {
-          constraints: [{to: 'window', pin: true, attachment: "both"}]
-        }
-      });
-
-      drop.open();
-
-      drop.on('close', function() {
-        setTimeout(function() {
-          drop.destroy();
-        });
-      });
-    }]);
-  }
-
-  var markerElementToAttachTo = null;
-
-  function createEditPopover(element, event, plot) {
-    var eventManager = plot.getOptions().events.manager;
-    if (eventManager.editorOpen) {
-      // update marker element to attach to (needed in case of legend on the right
-      // when there is a double render pass and the initial marker element is removed)
-      markerElementToAttachTo = element;
-      return;
-    }
-
-    // mark as openend
-    eventManager.editorOpened();
-    // set marker element to attache to
-    markerElementToAttachTo = element;
-
-    // wait for element to be attached and positioned
-    setTimeout(function() {
-
-      var injector = angular.element(document).injector();
-      var content = document.createElement('div');
-      content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
-
-      injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
-        var scope = $rootScope.$new(true);
-        var drop;
-
-        scope.event = event;
-        scope.panelCtrl = eventManager.panelCtrl;
-        scope.close = function() {
-          drop.close();
-        };
-
-        $compile(content)(scope);
-        scope.$digest();
-
-        drop = new Drop({
-          target: markerElementToAttachTo[0],
-          content: content,
-          position: "bottom center",
-          classes: 'drop-popover drop-popover--form',
-          openOn: 'click',
-          tetherOptions: {
-            constraints: [{to: 'window', pin: true, attachment: "both"}]
-          }
-        });
-
-        drop.open();
-        eventManager.editorOpened();
-
-        drop.on('close', function() {
-          // need timeout here in order call drop.destroy
-          setTimeout(function() {
-            eventManager.editorClosed();
-            scope.$destroy();
-            drop.destroy();
-          });
-        });
-      }]);
-
-    }, 100);
-  }
-
-  /*
-   * jquery.flot.events
-   *
-   * description: Flot plugin for adding events/markers to the plot
-   * version: 0.2.5
-   * authors:
-   *    Alexander Wunschik <alex@wunschik.net>
-   *    Joel Oughton <joeloughton@gmail.com>
-   *    Nicolas Joseph <www.nicolasjoseph.com>
-   *
-   * website: https://github.com/mojoaxel/flot-events
-   *
-   * released under MIT License and GPLv2+
-   */
-
-  /**
-   * A class that allows for the drawing an remove of some object
-   */
-  var DrawableEvent = function(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
-    var _object = object;
-    var	_drawFunc = drawFunc;
-    var	_clearFunc = clearFunc;
-    var	_moveFunc = moveFunc;
-    var	_position = { left: left, top: top };
-    var	_width = width;
-    var	_height = height;
-
-    this.width = function() { return _width; };
-    this.height = function() { return _height; };
-    this.position = function() { return _position; };
-    this.draw = function() { _drawFunc(_object); };
-    this.clear = function() { _clearFunc(_object); };
-    this.getObject = function() { return _object; };
-    this.moveTo = function(position) {
-      _position = position;
-      _moveFunc(_object, _position);
-    };
-  };
-
-  /**
-   * Event class that stores options (eventType, min, max, title, description) and the object to draw.
-   */
-  var VisualEvent = function(options, drawableEvent) {
-    var _parent;
-    var _options = options;
-    var _drawableEvent = drawableEvent;
-    var _hidden = false;
-
-    this.visual = function() { return _drawableEvent; };
-    this.getOptions = function() { return _options; };
-    this.getParent = function() { return _parent; };
-    this.isHidden = function() { return _hidden; };
-    this.hide = function() { _hidden = true; };
-    this.unhide = function() { _hidden = false; };
-  };
-
-  /**
-   * A Class that handles the event-markers inside the given plot
-   */
-  var EventMarkers = function(plot) {
-    var _events = [];
-
-    this._types = [];
-    this._plot = plot;
-    this.eventsEnabled = false;
-
-    this.getEvents = function() {
-      return _events;
-    };
-
-    this.setTypes = function(types) {
-      return this._types = types;
-    };
-
-    /**
-     * create internal objects for the given events
-     */
-    this.setupEvents = function(events) {
-      var that = this;
-      var parts = _.partition(events, 'isRegion');
-      var regions = parts[0];
-      events = parts[1];
-
-      $.each(events, function(index, event) {
-        var ve = new VisualEvent(event, that._buildDiv(event));
-        _events.push(ve);
-      });
-
-      $.each(regions, function (index, event) {
-        var vre = new VisualEvent(event, that._buildRegDiv(event));
-        _events.push(vre);
-      });
-
-      _events.sort(function(a, b) {
-        var ao = a.getOptions(), bo = b.getOptions();
-        if (ao.min > bo.min) { return 1; }
-        if (ao.min < bo.min) { return -1; }
-        return 0;
-      });
-    };
-
-    /**
-     * draw the events to the plot
-     */
-    this.drawEvents = function() {
-      var that = this;
-      // var o = this._plot.getPlotOffset();
-
-      $.each(_events, function(index, event) {
-        // check event is inside the graph range
-        if (that._insidePlot(event.getOptions().min) && !event.isHidden()) {
-          event.visual().draw();
-        }  else {
-          event.visual().getObject().hide();
-        }
-      });
-    };
-
-    /**
-     * update the position of the event-markers (e.g. after scrolling or zooming)
-     */
-    this.updateEvents = function() {
-      var that = this;
-      var o = this._plot.getPlotOffset(), left, top;
-      var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-
-      $.each(_events, function(index, event) {
-        top = o.top + that._plot.height() - event.visual().height();
-        left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
-        event.visual().moveTo({ top: top, left: left });
-      });
-    };
-
-    /**
-     * remove all events from the plot
-     */
-    this._clearEvents = function() {
-      $.each(_events, function(index, val) {
-        val.visual().clear();
-      });
-      _events = [];
-    };
-
-    /**
-     * create a DOM element for the given event
-     */
-    this._buildDiv = function(event) {
-      var that = this;
-
-      var container = this._plot.getPlaceholder();
-      var o = this._plot.getPlotOffset();
-      var axes = this._plot.getAxes();
-      var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-      var yaxis, top, left, color, markerSize, markerShow, lineStyle, lineWidth;
-      var markerTooltip;
-
-      // determine the y axis used
-      if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
-      if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
-
-      // map the eventType to a types object
-      var eventTypeId = event.eventType;
-
-      if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
-        color = '#666';
-      } else {
-        color = this._types[eventTypeId].color;
-      }
-
-      if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
-        markerSize = 8; //default marker size
-      } else {
-        markerSize = this._types[eventTypeId].markerSize;
-      }
-
-      if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
-        markerShow = true;
-      } else {
-        markerShow = this._types[eventTypeId].markerShow;
-      }
-
-      if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
-        markerTooltip = true;
-      } else {
-        markerTooltip = this._types[eventTypeId].markerTooltip;
-      }
-
-      if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
-        lineStyle = 'dashed'; //default line style
-      } else {
-        lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
-      }
-
-      if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
-        lineWidth = 1; //default line width
-      } else {
-        lineWidth = this._types[eventTypeId].lineWidth;
-      }
-
-      var topOffset = xaxis.options.eventSectionHeight || 0;
-      topOffset = topOffset / 3;
-
-      top = o.top + this._plot.height() + topOffset;
-      left = xaxis.p2c(event.min) + o.left;
-
-      var line = $('<div class="events_line flot-temp-elem"></div>').css({
-        "position": "absolute",
-        "opacity": 0.8,
-        "left": left + 'px',
-        "top": 8,
-        "width": lineWidth + "px",
-        "height": this._plot.height() + topOffset * 0.8,
-        "border-left-width": lineWidth + "px",
-        "border-left-style": lineStyle,
-        "border-left-color": color,
-        "color": color
-      })
-      .appendTo(container);
-
-      if (markerShow) {
-        var marker = $('<div class="events_marker"></div>').css({
-          "position": "absolute",
-          "left": (-markerSize - Math.round(lineWidth / 2)) + "px",
-          "font-size": 0,
-          "line-height": 0,
-          "width": 0,
-          "height": 0,
-          "border-left": markerSize+"px solid transparent",
-          "border-right": markerSize+"px solid transparent"
-        });
-
-        marker.appendTo(line);
-
-        if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') {
-          marker.css({
-            "top": top-markerSize-8 +"px",
-            "border-top": "none",
-            "border-bottom": markerSize+"px solid " + color
-          });
-        } else {
-          marker.css({
-            "top": "0px",
-            "border-top": markerSize+"px solid " + color,
-            "border-bottom": "none"
-          });
-        }
-
-        marker.data({
-          "event": event
-        });
-
-        var mouseenter = function() {
-          createAnnotationToolip(marker, $(this).data("event"), that._plot);
-        };
-
-        if (event.editModel) {
-          createEditPopover(marker, event.editModel, that._plot);
-        }
-
-        var mouseleave = function() {
-          that._plot.clearSelection();
-        };
-
-        if (markerTooltip) {
-          marker.css({ "cursor": "help" });
-          marker.hover(mouseenter, mouseleave);
-        }
-      }
-
-      var drawableEvent = new DrawableEvent(
-        line,
-        function drawFunc(obj) { obj.show(); },
-        function(obj) { obj.remove(); },
-        function(obj, position) {
-          obj.css({
-            top: position.top,
-            left: position.left
-          });
-        },
-        left,
-        top,
-        line.width(),
-        line.height()
-      );
-
-      return drawableEvent;
-    };
-
-    /**
-     * create a DOM element for the given region
-     */
-    this._buildRegDiv = function (event) {
-      var that = this;
-
-      var container = this._plot.getPlaceholder();
-      var o = this._plot.getPlotOffset();
-      var axes = this._plot.getAxes();
-      var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-      var yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
-
-      // determine the y axis used
-      if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
-      if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
-
-      // map the eventType to a types object
-      var eventTypeId = event.eventType;
-
-      if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
-        color = '#666';
-      } else {
-        color = this._types[eventTypeId].color;
-      }
-
-      if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
-        markerTooltip = true;
-      } else {
-        markerTooltip = this._types[eventTypeId].markerTooltip;
-      }
-
-      if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
-        lineWidth = 1; //default line width
-      } else {
-        lineWidth = this._types[eventTypeId].lineWidth;
-      }
-
-      if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
-        lineStyle = 'dashed'; //default line style
-      } else {
-        lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
-      }
-
-      var topOffset = 2;
-      top = o.top + this._plot.height() + topOffset;
-
-      var timeFrom = Math.min(event.min, event.timeEnd);
-      var timeTo = Math.max(event.min, event.timeEnd);
-      left = xaxis.p2c(timeFrom) + o.left;
-      var right = xaxis.p2c(timeTo) + o.left;
-      regionWidth = right - left;
-
-      _.each([left, right], function(position) {
-        var line = $('<div class="events_line flot-temp-elem"></div>').css({
-          "position": "absolute",
-          "opacity": 0.8,
-          "left": position + 'px',
-          "top": 8,
-          "width": lineWidth + "px",
-          "height": that._plot.height() + topOffset,
-          "border-left-width": lineWidth + "px",
-          "border-left-style": lineStyle,
-          "border-left-color": color,
-          "color": color
-        });
-        line.appendTo(container);
-      });
-
-      var region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
-        "position": "absolute",
-        "opacity": 0.5,
-        "left": left + 'px',
-        "top": top,
-        "width": Math.round(regionWidth + lineWidth) + "px",
-        "height": "0.5rem",
-        "border-left-color": color,
-        "color": color,
-        "background-color": color
-      });
-      region.appendTo(container);
-
-      region.data({
-        "event": event
-      });
-
-      var mouseenter = function () {
-        createAnnotationToolip(region, $(this).data("event"), that._plot);
-      };
-
-      if (event.editModel) {
-        createEditPopover(region, event.editModel, that._plot);
-      }
-
-      var mouseleave = function () {
-        that._plot.clearSelection();
-      };
-
-      if (markerTooltip) {
-        region.css({ "cursor": "help" });
-        region.hover(mouseenter, mouseleave);
-      }
-
-      var drawableEvent = new DrawableEvent(
-        region,
-        function drawFunc(obj) { obj.show(); },
-        function (obj) { obj.remove(); },
-        function (obj, position) {
-          obj.css({
-            top: position.top,
-            left: position.left
-          });
-        },
-        left,
-        top,
-        region.width(),
-        region.height()
-      );
-
-      return drawableEvent;
-    };
-
-    /**
-     * check if the event is inside visible range
-     */
-    this._insidePlot = function(x) {
-      var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-      var xc = xaxis.p2c(x);
-      return xc > 0 && xc < xaxis.p2c(xaxis.max);
-    };
-  };
-
-  /**
-   * initialize the plugin for the given plot
-   */
-  function init(plot) {
-    /*jshint validthis:true */
-    var that = this;
-    var eventMarkers = new EventMarkers(plot);
-
-    plot.getEvents = function() {
-      return eventMarkers._events;
-    };
-
-    plot.hideEvents = function() {
-      $.each(eventMarkers._events, function(index, event) {
-        event.visual().getObject().hide();
-      });
-    };
-
-    plot.showEvents = function() {
-      plot.hideEvents();
-      $.each(eventMarkers._events, function(index, event) {
-        event.hide();
-      });
-
-      that.eventMarkers.drawEvents();
-    };
-
-    // change events on an existing plot
-    plot.setEvents = function(events) {
-      if (eventMarkers.eventsEnabled) {
-        eventMarkers.setupEvents(events);
-      }
-    };
-
-    plot.hooks.processOptions.push(function(plot, options) {
-      // enable the plugin
-      if (options.events.data != null) {
-        eventMarkers.eventsEnabled = true;
-      }
-    });
-
-    plot.hooks.draw.push(function(plot) {
-      var options = plot.getOptions();
-
-      if (eventMarkers.eventsEnabled) {
-        // check for first run
-        if (eventMarkers.getEvents().length < 1) {
-          eventMarkers.setTypes(options.events.types);
-          eventMarkers.setupEvents(options.events.data);
-        } else {
-          eventMarkers.updateEvents();
-        }
-      }
-
-      eventMarkers.drawEvents();
-    });
-  }
-
-  var defaultOptions = {
-    events: {
-      data: null,
-      types: null,
-      xaxis: 1,
-      position: 'BOTTOM'
-    }
-  };
-
-  $.plot.plugins.push({
-    init: init,
-    options: defaultOptions,
-    name: "events",
-    version: "0.2.5"
-  });
-});
diff --git a/public/app/plugins/panel/graph/jquery.flot.events.ts b/public/app/plugins/panel/graph/jquery.flot.events.ts
new file mode 100644
index 00000000000..642883ff75c
--- /dev/null
+++ b/public/app/plugins/panel/graph/jquery.flot.events.ts
@@ -0,0 +1,663 @@
+import $ from 'jquery';
+import _ from 'lodash';
+import angular from 'angular';
+import Drop from 'tether-drop';
+
+function createAnnotationToolip(element, event, plot) {
+  let injector = angular.element(document).injector();
+  let content = document.createElement('div');
+  content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
+
+  injector.invoke([
+    '$compile',
+    '$rootScope',
+    function($compile, $rootScope) {
+      let eventManager = plot.getOptions().events.manager;
+      let tmpScope = $rootScope.$new(true);
+      tmpScope.event = event;
+      tmpScope.onEdit = function() {
+        eventManager.editEvent(event);
+      };
+
+      $compile(content)(tmpScope);
+      tmpScope.$digest();
+      tmpScope.$destroy();
+
+      let drop = new Drop({
+        target: element[0],
+        content: content,
+        position: 'bottom center',
+        classes: 'drop-popover drop-popover--annotation',
+        openOn: 'hover',
+        hoverCloseDelay: 200,
+        tetherOptions: {
+          constraints: [{ to: 'window', pin: true, attachment: 'both' }],
+        },
+      });
+
+      drop.open();
+
+      drop.on('close', function() {
+        setTimeout(function() {
+          drop.destroy();
+        });
+      });
+    },
+  ]);
+}
+
+let markerElementToAttachTo = null;
+
+function createEditPopover(element, event, plot) {
+  let eventManager = plot.getOptions().events.manager;
+  if (eventManager.editorOpen) {
+    // update marker element to attach to (needed in case of legend on the right
+    // when there is a double render pass and the inital marker element is removed)
+    markerElementToAttachTo = element;
+    return;
+  }
+
+  // mark as openend
+  eventManager.editorOpened();
+  // set marker elment to attache to
+  markerElementToAttachTo = element;
+
+  // wait for element to be attached and positioned
+  setTimeout(function() {
+    let injector = angular.element(document).injector();
+    let content = document.createElement('div');
+    content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
+
+    injector.invoke([
+      '$compile',
+      '$rootScope',
+      function($compile, $rootScope) {
+        let scope = $rootScope.$new(true);
+        let drop;
+
+        scope.event = event;
+        scope.panelCtrl = eventManager.panelCtrl;
+        scope.close = function() {
+          drop.close();
+        };
+
+        $compile(content)(scope);
+        scope.$digest();
+
+        drop = new Drop({
+          target: markerElementToAttachTo[0],
+          content: content,
+          position: 'bottom center',
+          classes: 'drop-popover drop-popover--form',
+          openOn: 'click',
+          tetherOptions: {
+            constraints: [{ to: 'window', pin: true, attachment: 'both' }],
+          },
+        });
+
+        drop.open();
+        eventManager.editorOpened();
+
+        drop.on('close', function() {
+          // need timeout here in order call drop.destroy
+          setTimeout(function() {
+            eventManager.editorClosed();
+            scope.$destroy();
+            drop.destroy();
+          });
+        });
+      },
+    ]);
+  }, 100);
+}
+
+/*
+ * jquery.flot.events
+ *
+ * description: Flot plugin for adding events/markers to the plot
+ * version: 0.2.5
+ * authors:
+ *    Alexander Wunschik <alex@wunschik.net>
+ *    Joel Oughton <joeloughton@gmail.com>
+ *    Nicolas Joseph <www.nicolasjoseph.com>
+ *
+ * website: https://github.com/mojoaxel/flot-events
+ *
+ * released under MIT License and GPLv2+
+ */
+
+/**
+ * A class that allows for the drawing an remove of some object
+ */
+let DrawableEvent = function(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
+  let _object = object;
+  let _drawFunc = drawFunc;
+  let _clearFunc = clearFunc;
+  let _moveFunc = moveFunc;
+  let _position = { left: left, top: top };
+  let _width = width;
+  let _height = height;
+
+  this.width = function() {
+    return _width;
+  };
+  this.height = function() {
+    return _height;
+  };
+  this.position = function() {
+    return _position;
+  };
+  this.draw = function() {
+    _drawFunc(_object);
+  };
+  this.clear = function() {
+    _clearFunc(_object);
+  };
+  this.getObject = function() {
+    return _object;
+  };
+  this.moveTo = function(position) {
+    _position = position;
+    _moveFunc(_object, _position);
+  };
+};
+
+/**
+ * Event class that stores options (eventType, min, max, title, description) and the object to draw.
+ */
+let VisualEvent = function(options, drawableEvent) {
+  let _parent;
+  let _options = options;
+  let _drawableEvent = drawableEvent;
+  let _hidden = false;
+
+  this.visual = function() {
+    return _drawableEvent;
+  };
+  this.getOptions = function() {
+    return _options;
+  };
+  this.getParent = function() {
+    return _parent;
+  };
+  this.isHidden = function() {
+    return _hidden;
+  };
+  this.hide = function() {
+    _hidden = true;
+  };
+  this.unhide = function() {
+    _hidden = false;
+  };
+};
+
+/**
+ * A Class that handles the event-markers inside the given plot
+ */
+let EventMarkers = function(plot) {
+  let _events = [];
+
+  this._types = [];
+  this._plot = plot;
+  this.eventsEnabled = false;
+
+  this.getEvents = function() {
+    return _events;
+  };
+
+  this.setTypes = function(types) {
+    return (this._types = types);
+  };
+
+  /**
+   * create internal objects for the given events
+   */
+  this.setupEvents = function(events) {
+    let that = this;
+    let parts = _.partition(events, 'isRegion');
+    let regions = parts[0];
+    events = parts[1];
+
+    $.each(events, function(index, event) {
+      let ve = new VisualEvent(event, that._buildDiv(event));
+      _events.push(ve);
+    });
+
+    $.each(regions, function(index, event) {
+      let vre = new VisualEvent(event, that._buildRegDiv(event));
+      _events.push(vre);
+    });
+
+    _events.sort(function(a, b) {
+      let ao = a.getOptions(),
+        bo = b.getOptions();
+      if (ao.min > bo.min) {
+        return 1;
+      }
+      if (ao.min < bo.min) {
+        return -1;
+      }
+      return 0;
+    });
+  };
+
+  /**
+   * draw the events to the plot
+   */
+  this.drawEvents = function() {
+    let that = this;
+    // let o = this._plot.getPlotOffset();
+
+    $.each(_events, function(index, event) {
+      // check event is inside the graph range
+      if (that._insidePlot(event.getOptions().min) && !event.isHidden()) {
+        event.visual().draw();
+      } else {
+        event
+          .visual()
+          .getObject()
+          .hide();
+      }
+    });
+  };
+
+  /**
+   * update the position of the event-markers (e.g. after scrolling or zooming)
+   */
+  this.updateEvents = function() {
+    let that = this;
+    let o = this._plot.getPlotOffset(),
+      left,
+      top;
+    let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
+
+    $.each(_events, function(index, event) {
+      top = o.top + that._plot.height() - event.visual().height();
+      left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
+      event.visual().moveTo({ top: top, left: left });
+    });
+  };
+
+  /**
+   * remove all events from the plot
+   */
+  this._clearEvents = function() {
+    $.each(_events, function(index, val) {
+      val.visual().clear();
+    });
+    _events = [];
+  };
+
+  /**
+   * create a DOM element for the given event
+   */
+  this._buildDiv = function(event) {
+    let that = this;
+
+    let container = this._plot.getPlaceholder();
+    let o = this._plot.getPlotOffset();
+    let axes = this._plot.getAxes();
+    let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
+    let yaxis, top, left, color, markerSize, markerShow, lineStyle, lineWidth;
+    let markerTooltip;
+
+    // determine the y axis used
+    if (axes.yaxis && axes.yaxis.used) {
+      yaxis = axes.yaxis;
+    }
+    if (axes.yaxis2 && axes.yaxis2.used) {
+      yaxis = axes.yaxis2;
+    }
+
+    // map the eventType to a types object
+    let eventTypeId = event.eventType;
+
+    if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
+      color = '#666';
+    } else {
+      color = this._types[eventTypeId].color;
+    }
+
+    if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
+      markerSize = 8; //default marker size
+    } else {
+      markerSize = this._types[eventTypeId].markerSize;
+    }
+
+    if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
+      markerShow = true;
+    } else {
+      markerShow = this._types[eventTypeId].markerShow;
+    }
+
+    if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
+      markerTooltip = true;
+    } else {
+      markerTooltip = this._types[eventTypeId].markerTooltip;
+    }
+
+    if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
+      lineStyle = 'dashed'; //default line style
+    } else {
+      lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
+    }
+
+    if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
+      lineWidth = 1; //default line width
+    } else {
+      lineWidth = this._types[eventTypeId].lineWidth;
+    }
+
+    let topOffset = xaxis.options.eventSectionHeight || 0;
+    topOffset = topOffset / 3;
+
+    top = o.top + this._plot.height() + topOffset;
+    left = xaxis.p2c(event.min) + o.left;
+
+    let line = $('<div class="events_line flot-temp-elem"></div>')
+      .css({
+        position: 'absolute',
+        opacity: 0.8,
+        left: left + 'px',
+        top: 8,
+        width: lineWidth + 'px',
+        height: this._plot.height() + topOffset * 0.8,
+        'border-left-width': lineWidth + 'px',
+        'border-left-style': lineStyle,
+        'border-left-color': color,
+        color: color,
+      })
+      .appendTo(container);
+
+    if (markerShow) {
+      let marker = $('<div class="events_marker"></div>').css({
+        position: 'absolute',
+        left: -markerSize - Math.round(lineWidth / 2) + 'px',
+        'font-size': 0,
+        'line-height': 0,
+        width: 0,
+        height: 0,
+        'border-left': markerSize + 'px solid transparent',
+        'border-right': markerSize + 'px solid transparent',
+      });
+
+      marker.appendTo(line);
+
+      if (
+        this._types[eventTypeId] &&
+        this._types[eventTypeId].position &&
+        this._types[eventTypeId].position.toUpperCase() === 'BOTTOM'
+      ) {
+        marker.css({
+          top: top - markerSize - 8 + 'px',
+          'border-top': 'none',
+          'border-bottom': markerSize + 'px solid ' + color,
+        });
+      } else {
+        marker.css({
+          top: '0px',
+          'border-top': markerSize + 'px solid ' + color,
+          'border-bottom': 'none',
+        });
+      }
+
+      marker.data({
+        event: event,
+      });
+
+      let mouseenter = function() {
+        createAnnotationToolip(marker, $(this).data('event'), that._plot);
+      };
+
+      if (event.editModel) {
+        createEditPopover(marker, event.editModel, that._plot);
+      }
+
+      let mouseleave = function() {
+        that._plot.clearSelection();
+      };
+
+      if (markerTooltip) {
+        marker.css({ cursor: 'help' });
+        marker.hover(mouseenter, mouseleave);
+      }
+    }
+
+    let drawableEvent = new DrawableEvent(
+      line,
+      function drawFunc(obj) {
+        obj.show();
+      },
+      function(obj) {
+        obj.remove();
+      },
+      function(obj, position) {
+        obj.css({
+          top: position.top,
+          left: position.left,
+        });
+      },
+      left,
+      top,
+      line.width(),
+      line.height()
+    );
+
+    return drawableEvent;
+  };
+
+  /**
+   * create a DOM element for the given region
+   */
+  this._buildRegDiv = function(event) {
+    let that = this;
+
+    let container = this._plot.getPlaceholder();
+    let o = this._plot.getPlotOffset();
+    let axes = this._plot.getAxes();
+    let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
+    let yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
+
+    // determine the y axis used
+    if (axes.yaxis && axes.yaxis.used) {
+      yaxis = axes.yaxis;
+    }
+    if (axes.yaxis2 && axes.yaxis2.used) {
+      yaxis = axes.yaxis2;
+    }
+
+    // map the eventType to a types object
+    let eventTypeId = event.eventType;
+
+    if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
+      color = '#666';
+    } else {
+      color = this._types[eventTypeId].color;
+    }
+
+    if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
+      markerTooltip = true;
+    } else {
+      markerTooltip = this._types[eventTypeId].markerTooltip;
+    }
+
+    if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
+      lineWidth = 1; //default line width
+    } else {
+      lineWidth = this._types[eventTypeId].lineWidth;
+    }
+
+    if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
+      lineStyle = 'dashed'; //default line style
+    } else {
+      lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
+    }
+
+    let topOffset = 2;
+    top = o.top + this._plot.height() + topOffset;
+
+    let timeFrom = Math.min(event.min, event.timeEnd);
+    let timeTo = Math.max(event.min, event.timeEnd);
+    left = xaxis.p2c(timeFrom) + o.left;
+    let right = xaxis.p2c(timeTo) + o.left;
+    regionWidth = right - left;
+
+    _.each([left, right], function(position) {
+      let line = $('<div class="events_line flot-temp-elem"></div>').css({
+        position: 'absolute',
+        opacity: 0.8,
+        left: position + 'px',
+        top: 8,
+        width: lineWidth + 'px',
+        height: that._plot.height() + topOffset,
+        'border-left-width': lineWidth + 'px',
+        'border-left-style': lineStyle,
+        'border-left-color': color,
+        color: color,
+      });
+      line.appendTo(container);
+    });
+
+    let region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
+      position: 'absolute',
+      opacity: 0.5,
+      left: left + 'px',
+      top: top,
+      width: Math.round(regionWidth + lineWidth) + 'px',
+      height: '0.5rem',
+      'border-left-color': color,
+      color: color,
+      'background-color': color,
+    });
+    region.appendTo(container);
+
+    region.data({
+      event: event,
+    });
+
+    let mouseenter = function() {
+      createAnnotationToolip(region, $(this).data('event'), that._plot);
+    };
+
+    if (event.editModel) {
+      createEditPopover(region, event.editModel, that._plot);
+    }
+
+    let mouseleave = function() {
+      that._plot.clearSelection();
+    };
+
+    if (markerTooltip) {
+      region.css({ cursor: 'help' });
+      region.hover(mouseenter, mouseleave);
+    }
+
+    let drawableEvent = new DrawableEvent(
+      region,
+      function drawFunc(obj) {
+        obj.show();
+      },
+      function(obj) {
+        obj.remove();
+      },
+      function(obj, position) {
+        obj.css({
+          top: position.top,
+          left: position.left,
+        });
+      },
+      left,
+      top,
+      region.width(),
+      region.height()
+    );
+
+    return drawableEvent;
+  };
+
+  /**
+   * check if the event is inside visible range
+   */
+  this._insidePlot = function(x) {
+    let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
+    let xc = xaxis.p2c(x);
+    return xc > 0 && xc < xaxis.p2c(xaxis.max);
+  };
+};
+
+/**
+ * initialize the plugin for the given plot
+ */
+function init(plot) {
+  /*jshint validthis:true */
+  let that = this;
+  let eventMarkers = new EventMarkers(plot);
+
+  plot.getEvents = function() {
+    return eventMarkers._events;
+  };
+
+  plot.hideEvents = function() {
+    $.each(eventMarkers._events, function(index, event) {
+      event
+        .visual()
+        .getObject()
+        .hide();
+    });
+  };
+
+  plot.showEvents = function() {
+    plot.hideEvents();
+    $.each(eventMarkers._events, function(index, event) {
+      event.hide();
+    });
+
+    that.eventMarkers.drawEvents();
+  };
+
+  // change events on an existing plot
+  plot.setEvents = function(events) {
+    if (eventMarkers.eventsEnabled) {
+      eventMarkers.setupEvents(events);
+    }
+  };
+
+  plot.hooks.processOptions.push(function(plot, options) {
+    // enable the plugin
+    if (options.events.data != null) {
+      eventMarkers.eventsEnabled = true;
+    }
+  });
+
+  plot.hooks.draw.push(function(plot) {
+    let options = plot.getOptions();
+
+    if (eventMarkers.eventsEnabled) {
+      // check for first run
+      if (eventMarkers.getEvents().length < 1) {
+        eventMarkers.setTypes(options.events.types);
+        eventMarkers.setupEvents(options.events.data);
+      } else {
+        eventMarkers.updateEvents();
+      }
+    }
+
+    eventMarkers.drawEvents();
+  });
+}
+
+let defaultOptions = {
+  events: {
+    data: null,
+    types: null,
+    xaxis: 1,
+    position: 'BOTTOM',
+  },
+};
+
+$.plot.plugins.push({
+  init: init,
+  options: defaultOptions,
+  name: 'events',
+  version: '0.2.5',
+});

From b2027af4cb3cdca3b84e3bf7547bf90ab38a240e Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Tue, 10 Apr 2018 14:16:56 +0200
Subject: [PATCH 002/380] wrote classes

---
 .../plugins/panel/graph/jquery.flot.events.ts | 258 +++++++++---------
 1 file changed, 133 insertions(+), 125 deletions(-)

diff --git a/public/app/plugins/panel/graph/jquery.flot.events.ts b/public/app/plugins/panel/graph/jquery.flot.events.ts
index 642883ff75c..9dfe0a8573f 100644
--- a/public/app/plugins/panel/graph/jquery.flot.events.ts
+++ b/public/app/plugins/panel/graph/jquery.flot.events.ts
@@ -1,9 +1,10 @@
+import angular from 'angular';
 import $ from 'jquery';
 import _ from 'lodash';
-import angular from 'angular';
 import Drop from 'tether-drop';
 
-function createAnnotationToolip(element, event, plot) {
+/** @ngInject */
+export function createAnnotationToolip(element, event, plot) {
   let injector = angular.element(document).injector();
   let content = document.createElement('div');
   content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
@@ -48,7 +49,8 @@ function createAnnotationToolip(element, event, plot) {
 
 let markerElementToAttachTo = null;
 
-function createEditPopover(element, event, plot) {
+/** @ngInject */
+export function createEditPopover(element, event, plot) {
   let eventManager = plot.getOptions().events.manager;
   if (eventManager.editorOpen) {
     // update marker element to attach to (needed in case of legend on the right
@@ -129,106 +131,130 @@ function createEditPopover(element, event, plot) {
 /**
  * A class that allows for the drawing an remove of some object
  */
-let DrawableEvent = function(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
-  let _object = object;
-  let _drawFunc = drawFunc;
-  let _clearFunc = clearFunc;
-  let _moveFunc = moveFunc;
-  let _position = { left: left, top: top };
-  let _width = width;
-  let _height = height;
+export class DrawableEvent {
+  _object: any;
+  _drawFunc: any;
+  _clearFunc: any;
+  _moveFunc: any;
+  _position: any;
+  _width: any;
+  _height: any;
 
-  this.width = function() {
-    return _width;
-  };
-  this.height = function() {
-    return _height;
-  };
-  this.position = function() {
-    return _position;
-  };
-  this.draw = function() {
-    _drawFunc(_object);
-  };
-  this.clear = function() {
-    _clearFunc(_object);
-  };
-  this.getObject = function() {
-    return _object;
-  };
-  this.moveTo = function(position) {
-    _position = position;
-    _moveFunc(_object, _position);
-  };
-};
+  /** @ngInject */
+  constructor(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
+    this._object = object;
+    this._drawFunc = drawFunc;
+    this._clearFunc = clearFunc;
+    this._moveFunc = moveFunc;
+    this._position = { left: left, top: top };
+    this._width = width;
+    this._height = height;
+  }
+
+  width() {
+    return this._width;
+  }
+  height() {
+    return this._height;
+  }
+  position() {
+    return this._position;
+  }
+  draw() {
+    this._drawFunc(this._object);
+  }
+  clear() {
+    this._clearFunc(this._object);
+  }
+  getObject() {
+    return this._object;
+  }
+  moveTo(position) {
+    this._position = position;
+    this._moveFunc(this._object, this._position);
+  }
+}
 
 /**
  * Event class that stores options (eventType, min, max, title, description) and the object to draw.
  */
-let VisualEvent = function(options, drawableEvent) {
-  let _parent;
-  let _options = options;
-  let _drawableEvent = drawableEvent;
-  let _hidden = false;
+export class VisualEvent {
+  _parent: any;
+  _options: any;
+  _drawableEvent: any;
+  _hidden: any;
 
-  this.visual = function() {
-    return _drawableEvent;
-  };
-  this.getOptions = function() {
-    return _options;
-  };
-  this.getParent = function() {
-    return _parent;
-  };
-  this.isHidden = function() {
-    return _hidden;
-  };
-  this.hide = function() {
-    _hidden = true;
-  };
-  this.unhide = function() {
-    _hidden = false;
-  };
-};
+  /** @ngInject */
+  constructor(options, drawableEvent) {
+    this._options = options;
+    this._drawableEvent = drawableEvent;
+    this._hidden = false;
+  }
+
+  visual() {
+    return this._drawableEvent;
+  }
+  getOptions() {
+    return this._options;
+  }
+  getParent() {
+    return this._parent;
+  }
+  isHidden() {
+    return this._hidden;
+  }
+  hide() {
+    this._hidden = true;
+  }
+  unhide() {
+    this._hidden = false;
+  }
+}
 
 /**
  * A Class that handles the event-markers inside the given plot
  */
-let EventMarkers = function(plot) {
-  let _events = [];
+export class EventMarkers {
+  _events: any;
+  _types: any;
+  _plot: any;
+  eventsEnabled: any;
 
-  this._types = [];
-  this._plot = plot;
-  this.eventsEnabled = false;
+  /** @ngInject */
+  constructor(plot) {
+    this._events = [];
+    this._types = [];
+    this._plot = plot;
+    this.eventsEnabled = false;
+  }
 
-  this.getEvents = function() {
-    return _events;
-  };
+  getEvents() {
+    return this._events;
+  }
 
-  this.setTypes = function(types) {
+  setTypes(types) {
     return (this._types = types);
-  };
+  }
 
   /**
    * create internal objects for the given events
    */
-  this.setupEvents = function(events) {
-    let that = this;
+  setupEvents(events) {
     let parts = _.partition(events, 'isRegion');
     let regions = parts[0];
     events = parts[1];
 
-    $.each(events, function(index, event) {
-      let ve = new VisualEvent(event, that._buildDiv(event));
-      _events.push(ve);
+    $.each(events, (index, event) => {
+      let ve = new VisualEvent(event, this._buildDiv(event));
+      this._events.push(ve);
     });
 
-    $.each(regions, function(index, event) {
-      let vre = new VisualEvent(event, that._buildRegDiv(event));
-      _events.push(vre);
+    $.each(regions, (index, event) => {
+      let vre = new VisualEvent(event, this._buildRegDiv(event));
+      this._events.push(vre);
     });
 
-    _events.sort(function(a, b) {
+    this._events.sort((a, b) => {
       let ao = a.getOptions(),
         bo = b.getOptions();
       if (ao.min > bo.min) {
@@ -239,18 +265,17 @@ let EventMarkers = function(plot) {
       }
       return 0;
     });
-  };
+  }
 
   /**
    * draw the events to the plot
    */
-  this.drawEvents = function() {
-    let that = this;
-    // let o = this._plot.getPlotOffset();
+  drawEvents() {
+    // var o = this._plot.getPlotOffset();
 
-    $.each(_events, function(index, event) {
+    $.each(this._events, (index, event) => {
       // check event is inside the graph range
-      if (that._insidePlot(event.getOptions().min) && !event.isHidden()) {
+      if (this._insidePlot(event.getOptions().min) && !event.isHidden()) {
         event.visual().draw();
       } else {
         event
@@ -259,56 +284,46 @@ let EventMarkers = function(plot) {
           .hide();
       }
     });
-  };
+  }
 
   /**
    * update the position of the event-markers (e.g. after scrolling or zooming)
    */
-  this.updateEvents = function() {
-    let that = this;
+  updateEvents() {
     let o = this._plot.getPlotOffset(),
       left,
       top;
     let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
 
-    $.each(_events, function(index, event) {
-      top = o.top + that._plot.height() - event.visual().height();
+    $.each(this._events, (index, event) => {
+      top = o.top + this._plot.height() - event.visual().height();
       left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
       event.visual().moveTo({ top: top, left: left });
     });
-  };
+  }
 
   /**
    * remove all events from the plot
    */
-  this._clearEvents = function() {
-    $.each(_events, function(index, val) {
+  _clearEvents() {
+    $.each(this._events, (index, val) => {
       val.visual().clear();
     });
-    _events = [];
-  };
+    this._events = [];
+  }
 
   /**
    * create a DOM element for the given event
    */
-  this._buildDiv = function(event) {
+  _buildDiv(event) {
     let that = this;
 
     let container = this._plot.getPlaceholder();
     let o = this._plot.getPlotOffset();
-    let axes = this._plot.getAxes();
     let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-    let yaxis, top, left, color, markerSize, markerShow, lineStyle, lineWidth;
+    let top, left, color, markerSize, markerShow, lineStyle, lineWidth;
     let markerTooltip;
 
-    // determine the y axis used
-    if (axes.yaxis && axes.yaxis.used) {
-      yaxis = axes.yaxis;
-    }
-    if (axes.yaxis2 && axes.yaxis2.used) {
-      yaxis = axes.yaxis2;
-    }
-
     // map the eventType to a types object
     let eventTypeId = event.eventType;
 
@@ -444,27 +459,18 @@ let EventMarkers = function(plot) {
     );
 
     return drawableEvent;
-  };
+  }
 
   /**
    * create a DOM element for the given region
    */
-  this._buildRegDiv = function(event) {
+  _buildRegDiv(event) {
     let that = this;
 
     let container = this._plot.getPlaceholder();
     let o = this._plot.getPlotOffset();
-    let axes = this._plot.getAxes();
     let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-    let yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
-
-    // determine the y axis used
-    if (axes.yaxis && axes.yaxis.used) {
-      yaxis = axes.yaxis;
-    }
-    if (axes.yaxis2 && axes.yaxis2.used) {
-      yaxis = axes.yaxis2;
-    }
+    let top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
 
     // map the eventType to a types object
     let eventTypeId = event.eventType;
@@ -502,14 +508,14 @@ let EventMarkers = function(plot) {
     let right = xaxis.p2c(timeTo) + o.left;
     regionWidth = right - left;
 
-    _.each([left, right], function(position) {
+    _.each([left, right], position => {
       let line = $('<div class="events_line flot-temp-elem"></div>').css({
         position: 'absolute',
         opacity: 0.8,
         left: position + 'px',
         top: 8,
         width: lineWidth + 'px',
-        height: that._plot.height() + topOffset,
+        height: this._plot.height() + topOffset,
         'border-left-width': lineWidth + 'px',
         'border-left-style': lineStyle,
         'border-left-color': color,
@@ -573,22 +579,24 @@ let EventMarkers = function(plot) {
     );
 
     return drawableEvent;
-  };
+  }
 
   /**
    * check if the event is inside visible range
    */
-  this._insidePlot = function(x) {
+  _insidePlot(x) {
     let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
     let xc = xaxis.p2c(x);
     return xc > 0 && xc < xaxis.p2c(xaxis.max);
-  };
-};
+  }
+}
 
 /**
  * initialize the plugin for the given plot
  */
-function init(plot) {
+
+/** @ngInject */
+export function init(plot) {
   /*jshint validthis:true */
   let that = this;
   let eventMarkers = new EventMarkers(plot);
@@ -598,7 +606,7 @@ function init(plot) {
   };
 
   plot.hideEvents = function() {
-    $.each(eventMarkers._events, function(index, event) {
+    $.each(eventMarkers._events, (index, event) => {
       event
         .visual()
         .getObject()
@@ -608,7 +616,7 @@ function init(plot) {
 
   plot.showEvents = function() {
     plot.hideEvents();
-    $.each(eventMarkers._events, function(index, event) {
+    $.each(eventMarkers._events, (index, event) => {
       event.hide();
     });
 

From acdc2bf100348530d7f8630d78da85434c8be8d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Francisco=20Guimar=C3=A3es?= <francisco.cpg@gmail.com>
Date: Fri, 15 Jun 2018 10:11:32 -0300
Subject: [PATCH 003/380] Adding Cloudwatch AWS/AppSync metrics and dimensions

---
 pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index 136ee241c2e..12c2aba4681 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -86,6 +86,7 @@ func init() {
 		"AWS/Kinesis":          {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"},
 		"AWS/KinesisAnalytics": {"Bytes", "MillisBehindLatest", "Records", "Success"},
 		"AWS/Lambda":           {"Invocations", "Errors", "Duration", "Throttles", "IteratorAge"},
+		"AWS/AppSync":          {"Latency", "4XXError", "5XXError"},
 		"AWS/Logs":             {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
 		"AWS/ML":               {"PredictCount", "PredictFailureCount"},
 		"AWS/NATGateway":       {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
@@ -135,6 +136,7 @@ func init() {
 		"AWS/Kinesis":          {"StreamName", "ShardId"},
 		"AWS/KinesisAnalytics": {"Flow", "Id", "Application"},
 		"AWS/Lambda":           {"FunctionName", "Resource", "Version", "Alias"},
+		"AWS/AppSync":          {"GraphQLAPIId"},
 		"AWS/Logs":             {"LogGroupName", "DestinationType", "FilterName"},
 		"AWS/ML":               {"MLModelId", "RequestMode"},
 		"AWS/NATGateway":       {"NatGatewayId"},

From 664944980a86c7082ce20e007a8789e949543453 Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Mon, 16 Apr 2018 15:27:25 +0900
Subject: [PATCH 004/380] update aws-sdk-go

---
 Gopkg.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gopkg.toml b/Gopkg.toml
index 1768059f0b8..6c91ec37221 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -36,7 +36,7 @@ ignored = [
 
 [[constraint]]
   name = "github.com/aws/aws-sdk-go"
-  version = "1.12.65"
+  version = "1.13.56"
 
 [[constraint]]
   branch = "master"

From 077cf9a343dcc2f0d54607776cb998404870d565 Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Mon, 16 Apr 2018 15:32:39 +0900
Subject: [PATCH 005/380] dep ensure

---
 Gopkg.lock                                    |  15 +-
 .../aws/aws-sdk-go/aws/client/client.go       |   4 +-
 .../aws/aws-sdk-go/aws/client/logger.go       | 102 ++-
 .../aws/client/metadata/client_info.go        |   1 +
 .../aws-sdk-go/aws/credentials/credentials.go |  18 +-
 .../github.com/aws/aws-sdk-go/aws/csm/doc.go  |  46 +
 .../aws/aws-sdk-go/aws/csm/enable.go          |  67 ++
 .../aws/aws-sdk-go/aws/csm/metric.go          |  51 ++
 .../aws/aws-sdk-go/aws/csm/metricChan.go      |  54 ++
 .../aws/aws-sdk-go/aws/csm/reporter.go        | 230 +++++
 .../aws/aws-sdk-go/aws/endpoints/defaults.go  |  75 +-
 .../github.com/aws/aws-sdk-go/aws/logger.go   |   6 +
 .../aws/aws-sdk-go/aws/request/handlers.go    |  18 +
 .../aws/aws-sdk-go/aws/request/request.go     |   9 +-
 .../aws/aws-sdk-go/aws/request/request_1_7.go |   2 +-
 .../aws/aws-sdk-go/aws/request/request_1_8.go |   2 +-
 .../aws/request/request_pagination.go         |  15 +-
 .../aws/aws-sdk-go/aws/session/env_config.go  |  20 +
 .../aws/aws-sdk-go/aws/session/session.go     |  26 +-
 .../aws/aws-sdk-go/aws/signer/v4/v4.go        |  13 +-
 .../github.com/aws/aws-sdk-go/aws/version.go  |   2 +-
 .../private/protocol/eventstream/debug.go     | 144 ++++
 .../private/protocol/eventstream/decode.go    | 199 +++++
 .../private/protocol/eventstream/encode.go    | 114 +++
 .../private/protocol/eventstream/error.go     |  23 +
 .../eventstream/eventstreamapi/api.go         | 160 ++++
 .../eventstream/eventstreamapi/error.go       |  24 +
 .../private/protocol/eventstream/header.go    | 166 ++++
 .../protocol/eventstream/header_value.go      | 501 +++++++++++
 .../private/protocol/eventstream/message.go   | 103 +++
 .../aws-sdk-go/private/protocol/payload.go    |  81 ++
 .../aws-sdk-go/private/protocol/rest/build.go |   8 +-
 .../private/protocol/rest/unmarshal.go        |   2 +-
 .../aws-sdk-go/service/cloudwatch/service.go  |   6 +-
 .../aws/aws-sdk-go/service/ec2/api.go         |  94 ++-
 .../aws/aws-sdk-go/service/ec2/service.go     |   6 +-
 .../aws/aws-sdk-go/service/s3/api.go          | 796 ++++++++++++++++++
 .../aws/aws-sdk-go/service/s3/service.go      |   8 +-
 .../aws/aws-sdk-go/service/sts/service.go     |   6 +-
 .../shurcooL/sanitized_anchor_name/LICENSE    |  21 +
 .../shurcooL/sanitized_anchor_name/main.go    |  29 +
 41 files changed, 3183 insertions(+), 84 deletions(-)
 create mode 100644 vendor/github.com/aws/aws-sdk-go/aws/csm/doc.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/aws/csm/enable.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/aws/csm/metric.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/aws/csm/metricChan.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/aws/csm/reporter.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/debug.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/decode.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/encode.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/error.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi/api.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi/error.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/header.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/header_value.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/message.go
 create mode 100644 vendor/github.com/aws/aws-sdk-go/private/protocol/payload.go
 create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE
 create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/main.go

diff --git a/Gopkg.lock b/Gopkg.lock
index 5acaf2a542c..6f08e208ecd 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -32,6 +32,7 @@
     "aws/credentials/ec2rolecreds",
     "aws/credentials/endpointcreds",
     "aws/credentials/stscreds",
+    "aws/csm",
     "aws/defaults",
     "aws/ec2metadata",
     "aws/endpoints",
@@ -43,6 +44,8 @@
     "internal/shareddefaults",
     "private/protocol",
     "private/protocol/ec2query",
+    "private/protocol/eventstream",
+    "private/protocol/eventstream/eventstreamapi",
     "private/protocol/query",
     "private/protocol/query/queryutil",
     "private/protocol/rest",
@@ -54,8 +57,8 @@
     "service/s3",
     "service/sts"
   ]
-  revision = "c7cd1ebe87257cde9b65112fc876b0339ea0ac30"
-  version = "v1.13.49"
+  revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
+  version = "v1.14.12"
 
 [[projects]]
   branch = "master"
@@ -424,6 +427,12 @@
   revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
   version = "v1.0.0"
 
+[[projects]]
+  branch = "master"
+  name = "github.com/shurcooL/sanitized_anchor_name"
+  packages = ["."]
+  revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
+
 [[projects]]
   name = "github.com/smartystreets/assertions"
   packages = [
@@ -670,6 +679,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7"
+  inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/client/client.go b/vendor/github.com/aws/aws-sdk-go/aws/client/client.go
index 3271a18e80e..212fe25e71e 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/client/client.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/client/client.go
@@ -91,6 +91,6 @@ func (c *Client) AddDebugHandlers() {
 		return
 	}
 
-	c.Handlers.Send.PushFrontNamed(request.NamedHandler{Name: "awssdk.client.LogRequest", Fn: logRequest})
-	c.Handlers.Send.PushBackNamed(request.NamedHandler{Name: "awssdk.client.LogResponse", Fn: logResponse})
+	c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
+	c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
 }
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/client/logger.go b/vendor/github.com/aws/aws-sdk-go/aws/client/logger.go
index e223c54cc6c..ce9fb896d94 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/client/logger.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/client/logger.go
@@ -44,12 +44,22 @@ func (reader *teeReaderCloser) Close() error {
 	return reader.Source.Close()
 }
 
+// LogHTTPRequestHandler is a SDK request handler to log the HTTP request sent
+// to a service. Will include the HTTP request body if the LogLevel of the
+// request matches LogDebugWithHTTPBody.
+var LogHTTPRequestHandler = request.NamedHandler{
+	Name: "awssdk.client.LogRequest",
+	Fn:   logRequest,
+}
+
 func logRequest(r *request.Request) {
 	logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
 	bodySeekable := aws.IsReaderSeekable(r.Body)
-	dumpedBody, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
+
+	b, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
 	if err != nil {
-		r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg, r.ClientInfo.ServiceName, r.Operation.Name, err))
+		r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
+			r.ClientInfo.ServiceName, r.Operation.Name, err))
 		return
 	}
 
@@ -63,7 +73,28 @@ func logRequest(r *request.Request) {
 		r.ResetBody()
 	}
 
-	r.Config.Logger.Log(fmt.Sprintf(logReqMsg, r.ClientInfo.ServiceName, r.Operation.Name, string(dumpedBody)))
+	r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
+		r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
+}
+
+// LogHTTPRequestHeaderHandler is a SDK request handler to log the HTTP request sent
+// to a service. Will only log the HTTP request's headers. The request payload
+// will not be read.
+var LogHTTPRequestHeaderHandler = request.NamedHandler{
+	Name: "awssdk.client.LogRequestHeader",
+	Fn:   logRequestHeader,
+}
+
+func logRequestHeader(r *request.Request) {
+	b, err := httputil.DumpRequestOut(r.HTTPRequest, false)
+	if err != nil {
+		r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
+			r.ClientInfo.ServiceName, r.Operation.Name, err))
+		return
+	}
+
+	r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
+		r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
 }
 
 const logRespMsg = `DEBUG: Response %s/%s Details:
@@ -76,27 +107,44 @@ const logRespErrMsg = `DEBUG ERROR: Response %s/%s:
 %s
 -----------------------------------------------------`
 
+// LogHTTPResponseHandler is a SDK request handler to log the HTTP response
+// received from a service. Will include the HTTP response body if the LogLevel
+// of the request matches LogDebugWithHTTPBody.
+var LogHTTPResponseHandler = request.NamedHandler{
+	Name: "awssdk.client.LogResponse",
+	Fn:   logResponse,
+}
+
 func logResponse(r *request.Request) {
 	lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
-	r.HTTPResponse.Body = &teeReaderCloser{
-		Reader: io.TeeReader(r.HTTPResponse.Body, lw),
-		Source: r.HTTPResponse.Body,
+
+	logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
+	if logBody {
+		r.HTTPResponse.Body = &teeReaderCloser{
+			Reader: io.TeeReader(r.HTTPResponse.Body, lw),
+			Source: r.HTTPResponse.Body,
+		}
 	}
 
 	handlerFn := func(req *request.Request) {
-		body, err := httputil.DumpResponse(req.HTTPResponse, false)
+		b, err := httputil.DumpResponse(req.HTTPResponse, false)
 		if err != nil {
-			lw.Logger.Log(fmt.Sprintf(logRespErrMsg, req.ClientInfo.ServiceName, req.Operation.Name, err))
+			lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
+				req.ClientInfo.ServiceName, req.Operation.Name, err))
 			return
 		}
 
-		b, err := ioutil.ReadAll(lw.buf)
-		if err != nil {
-			lw.Logger.Log(fmt.Sprintf(logRespErrMsg, req.ClientInfo.ServiceName, req.Operation.Name, err))
-			return
-		}
-		lw.Logger.Log(fmt.Sprintf(logRespMsg, req.ClientInfo.ServiceName, req.Operation.Name, string(body)))
-		if req.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody) {
+		lw.Logger.Log(fmt.Sprintf(logRespMsg,
+			req.ClientInfo.ServiceName, req.Operation.Name, string(b)))
+
+		if logBody {
+			b, err := ioutil.ReadAll(lw.buf)
+			if err != nil {
+				lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
+					req.ClientInfo.ServiceName, req.Operation.Name, err))
+				return
+			}
+
 			lw.Logger.Log(string(b))
 		}
 	}
@@ -110,3 +158,27 @@ func logResponse(r *request.Request) {
 		Name: handlerName, Fn: handlerFn,
 	})
 }
+
+// LogHTTPResponseHeaderHandler is a SDK request handler to log the HTTP
+// response received from a service. Will only log the HTTP response's headers.
+// The response payload will not be read.
+var LogHTTPResponseHeaderHandler = request.NamedHandler{
+	Name: "awssdk.client.LogResponseHeader",
+	Fn:   logResponseHeader,
+}
+
+func logResponseHeader(r *request.Request) {
+	if r.Config.Logger == nil {
+		return
+	}
+
+	b, err := httputil.DumpResponse(r.HTTPResponse, false)
+	if err != nil {
+		r.Config.Logger.Log(fmt.Sprintf(logRespErrMsg,
+			r.ClientInfo.ServiceName, r.Operation.Name, err))
+		return
+	}
+
+	r.Config.Logger.Log(fmt.Sprintf(logRespMsg,
+		r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go b/vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go
index 4778056ddfd..920e9fddf87 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go
@@ -3,6 +3,7 @@ package metadata
 // ClientInfo wraps immutable data from the client.Client structure.
 type ClientInfo struct {
 	ServiceName   string
+	ServiceID     string
 	APIVersion    string
 	Endpoint      string
 	SigningName   string
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go b/vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go
index 42416fc2f0f..ed086992f62 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go
@@ -178,7 +178,8 @@ func (e *Expiry) IsExpired() bool {
 type Credentials struct {
 	creds        Value
 	forceRefresh bool
-	m            sync.Mutex
+
+	m sync.RWMutex
 
 	provider Provider
 }
@@ -201,6 +202,17 @@ func NewCredentials(provider Provider) *Credentials {
 // If Credentials.Expire() was called the credentials Value will be force
 // expired, and the next call to Get() will cause them to be refreshed.
 func (c *Credentials) Get() (Value, error) {
+	// Check the cached credentials first with just the read lock.
+	c.m.RLock()
+	if !c.isExpired() {
+		creds := c.creds
+		c.m.RUnlock()
+		return creds, nil
+	}
+	c.m.RUnlock()
+
+	// Credentials are expired need to retrieve the credentials taking the full
+	// lock.
 	c.m.Lock()
 	defer c.m.Unlock()
 
@@ -234,8 +246,8 @@ func (c *Credentials) Expire() {
 // If the Credentials were forced to be expired with Expire() this will
 // reflect that override.
 func (c *Credentials) IsExpired() bool {
-	c.m.Lock()
-	defer c.m.Unlock()
+	c.m.RLock()
+	defer c.m.RUnlock()
 
 	return c.isExpired()
 }
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/csm/doc.go b/vendor/github.com/aws/aws-sdk-go/aws/csm/doc.go
new file mode 100644
index 00000000000..152d785b362
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/aws/csm/doc.go
@@ -0,0 +1,46 @@
+// Package csm provides Client Side Monitoring (CSM) which enables sending metrics
+// via UDP connection. Using the Start function will enable the reporting of
+// metrics on a given port. If Start is called, with different parameters, again,
+// a panic will occur.
+//
+// Pause can be called to pause any metrics publishing on a given port. Sessions
+// that have had their handlers modified via InjectHandlers may still be used.
+// However, the handlers will act as a no-op meaning no metrics will be published.
+//
+//	Example:
+//		r, err := csm.Start("clientID", ":31000")
+//		if err != nil {
+//			panic(fmt.Errorf("failed starting CSM:  %v", err))
+//		}
+//
+//		sess, err := session.NewSession(&aws.Config{})
+//		if err != nil {
+//			panic(fmt.Errorf("failed loading session: %v", err))
+//		}
+//
+//		r.InjectHandlers(&sess.Handlers)
+//
+//		client := s3.New(sess)
+//		resp, err := client.GetObject(&s3.GetObjectInput{
+//			Bucket: aws.String("bucket"),
+//			Key: aws.String("key"),
+//		})
+//
+//		// Will pause monitoring
+//		r.Pause()
+//		resp, err = client.GetObject(&s3.GetObjectInput{
+//			Bucket: aws.String("bucket"),
+//			Key: aws.String("key"),
+//		})
+//
+//		// Resume monitoring
+//		r.Continue()
+//
+// Start returns a Reporter that is used to enable or disable monitoring. If
+// access to the Reporter is required later, calling Get will return the Reporter
+// singleton.
+//
+//	Example:
+//		r := csm.Get()
+//		r.Continue()
+package csm
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/csm/enable.go b/vendor/github.com/aws/aws-sdk-go/aws/csm/enable.go
new file mode 100644
index 00000000000..2f0c6eac9a8
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/aws/csm/enable.go
@@ -0,0 +1,67 @@
+package csm
+
+import (
+	"fmt"
+	"sync"
+)
+
+var (
+	lock sync.Mutex
+)
+
+// Client side metric handler names
+const (
+	APICallMetricHandlerName        = "awscsm.SendAPICallMetric"
+	APICallAttemptMetricHandlerName = "awscsm.SendAPICallAttemptMetric"
+)
+
+// Start will start the a long running go routine to capture
+// client side metrics. Calling start multiple time will only
+// start the metric listener once and will panic if a different
+// client ID or port is passed in.
+//
+//	Example:
+//		r, err := csm.Start("clientID", "127.0.0.1:8094")
+//		if err != nil {
+//			panic(fmt.Errorf("expected no error, but received %v", err))
+//		}
+//		sess := session.NewSession()
+//		r.InjectHandlers(sess.Handlers)
+//
+//		svc := s3.New(sess)
+//		out, err := svc.GetObject(&s3.GetObjectInput{
+//			Bucket: aws.String("bucket"),
+//			Key: aws.String("key"),
+//		})
+func Start(clientID string, url string) (*Reporter, error) {
+	lock.Lock()
+	defer lock.Unlock()
+
+	if sender == nil {
+		sender = newReporter(clientID, url)
+	} else {
+		if sender.clientID != clientID {
+			panic(fmt.Errorf("inconsistent client IDs. %q was expected, but received %q", sender.clientID, clientID))
+		}
+
+		if sender.url != url {
+			panic(fmt.Errorf("inconsistent URLs. %q was expected, but received %q", sender.url, url))
+		}
+	}
+
+	if err := connect(url); err != nil {
+		sender = nil
+		return nil, err
+	}
+
+	return sender, nil
+}
+
+// Get will return a reporter if one exists, if one does not exist, nil will
+// be returned.
+func Get() *Reporter {
+	lock.Lock()
+	defer lock.Unlock()
+
+	return sender
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/csm/metric.go b/vendor/github.com/aws/aws-sdk-go/aws/csm/metric.go
new file mode 100644
index 00000000000..4b0d630e4c1
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/aws/csm/metric.go
@@ -0,0 +1,51 @@
+package csm
+
+import (
+	"strconv"
+	"time"
+)
+
+type metricTime time.Time
+
+func (t metricTime) MarshalJSON() ([]byte, error) {
+	ns := time.Duration(time.Time(t).UnixNano())
+	return []byte(strconv.FormatInt(int64(ns/time.Millisecond), 10)), nil
+}
+
+type metric struct {
+	ClientID  *string     `json:"ClientId,omitempty"`
+	API       *string     `json:"Api,omitempty"`
+	Service   *string     `json:"Service,omitempty"`
+	Timestamp *metricTime `json:"Timestamp,omitempty"`
+	Type      *string     `json:"Type,omitempty"`
+	Version   *int        `json:"Version,omitempty"`
+
+	AttemptCount *int `json:"AttemptCount,omitempty"`
+	Latency      *int `json:"Latency,omitempty"`
+
+	Fqdn           *string `json:"Fqdn,omitempty"`
+	UserAgent      *string `json:"UserAgent,omitempty"`
+	AttemptLatency *int    `json:"AttemptLatency,omitempty"`
+
+	SessionToken   *string `json:"SessionToken,omitempty"`
+	Region         *string `json:"Region,omitempty"`
+	AccessKey      *string `json:"AccessKey,omitempty"`
+	HTTPStatusCode *int    `json:"HttpStatusCode,omitempty"`
+	XAmzID2        *string `json:"XAmzId2,omitempty"`
+	XAmzRequestID  *string `json:"XAmznRequestId,omitempty"`
+
+	AWSException        *string `json:"AwsException,omitempty"`
+	AWSExceptionMessage *string `json:"AwsExceptionMessage,omitempty"`
+	SDKException        *string `json:"SdkException,omitempty"`
+	SDKExceptionMessage *string `json:"SdkExceptionMessage,omitempty"`
+
+	DestinationIP    *string `json:"DestinationIp,omitempty"`
+	ConnectionReused *int    `json:"ConnectionReused,omitempty"`
+
+	AcquireConnectionLatency *int `json:"AcquireConnectionLatency,omitempty"`
+	ConnectLatency           *int `json:"ConnectLatency,omitempty"`
+	RequestLatency           *int `json:"RequestLatency,omitempty"`
+	DNSLatency               *int `json:"DnsLatency,omitempty"`
+	TCPLatency               *int `json:"TcpLatency,omitempty"`
+	SSLLatency               *int `json:"SslLatency,omitempty"`
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/csm/metricChan.go b/vendor/github.com/aws/aws-sdk-go/aws/csm/metricChan.go
new file mode 100644
index 00000000000..514fc3739a5
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/aws/csm/metricChan.go
@@ -0,0 +1,54 @@
+package csm
+
+import (
+	"sync/atomic"
+)
+
+const (
+	runningEnum = iota
+	pausedEnum
+)
+
+var (
+	// MetricsChannelSize of metrics to hold in the channel
+	MetricsChannelSize = 100
+)
+
+type metricChan struct {
+	ch     chan metric
+	paused int64
+}
+
+func newMetricChan(size int) metricChan {
+	return metricChan{
+		ch: make(chan metric, size),
+	}
+}
+
+func (ch *metricChan) Pause() {
+	atomic.StoreInt64(&ch.paused, pausedEnum)
+}
+
+func (ch *metricChan) Continue() {
+	atomic.StoreInt64(&ch.paused, runningEnum)
+}
+
+func (ch *metricChan) IsPaused() bool {
+	v := atomic.LoadInt64(&ch.paused)
+	return v == pausedEnum
+}
+
+// Push will push metrics to the metric channel if the channel
+// is not paused
+func (ch *metricChan) Push(m metric) bool {
+	if ch.IsPaused() {
+		return false
+	}
+
+	select {
+	case ch.ch <- m:
+		return true
+	default:
+		return false
+	}
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/csm/reporter.go b/vendor/github.com/aws/aws-sdk-go/aws/csm/reporter.go
new file mode 100644
index 00000000000..1484c8fc5b1
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/aws/csm/reporter.go
@@ -0,0 +1,230 @@
+package csm
+
+import (
+	"encoding/json"
+	"net"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/awserr"
+	"github.com/aws/aws-sdk-go/aws/request"
+)
+
+const (
+	// DefaultPort is used when no port is specified
+	DefaultPort = "31000"
+)
+
+// Reporter will gather metrics of API requests made and
+// send those metrics to the CSM endpoint.
+type Reporter struct {
+	clientID  string
+	url       string
+	conn      net.Conn
+	metricsCh metricChan
+	done      chan struct{}
+}
+
+var (
+	sender *Reporter
+)
+
+func connect(url string) error {
+	const network = "udp"
+	if err := sender.connect(network, url); err != nil {
+		return err
+	}
+
+	if sender.done == nil {
+		sender.done = make(chan struct{})
+		go sender.start()
+	}
+
+	return nil
+}
+
+func newReporter(clientID, url string) *Reporter {
+	return &Reporter{
+		clientID:  clientID,
+		url:       url,
+		metricsCh: newMetricChan(MetricsChannelSize),
+	}
+}
+
+func (rep *Reporter) sendAPICallAttemptMetric(r *request.Request) {
+	if rep == nil {
+		return
+	}
+
+	now := time.Now()
+	creds, _ := r.Config.Credentials.Get()
+
+	m := metric{
+		ClientID:  aws.String(rep.clientID),
+		API:       aws.String(r.Operation.Name),
+		Service:   aws.String(r.ClientInfo.ServiceID),
+		Timestamp: (*metricTime)(&now),
+		UserAgent: aws.String(r.HTTPRequest.Header.Get("User-Agent")),
+		Region:    r.Config.Region,
+		Type:      aws.String("ApiCallAttempt"),
+		Version:   aws.Int(1),
+
+		XAmzRequestID: aws.String(r.RequestID),
+
+		AttemptCount:   aws.Int(r.RetryCount + 1),
+		AttemptLatency: aws.Int(int(now.Sub(r.AttemptTime).Nanoseconds() / int64(time.Millisecond))),
+		AccessKey:      aws.String(creds.AccessKeyID),
+	}
+
+	if r.HTTPResponse != nil {
+		m.HTTPStatusCode = aws.Int(r.HTTPResponse.StatusCode)
+	}
+
+	if r.Error != nil {
+		if awserr, ok := r.Error.(awserr.Error); ok {
+			setError(&m, awserr)
+		}
+	}
+
+	rep.metricsCh.Push(m)
+}
+
+func setError(m *metric, err awserr.Error) {
+	msg := err.Message()
+	code := err.Code()
+
+	switch code {
+	case "RequestError",
+		"SerializationError",
+		request.CanceledErrorCode:
+
+		m.SDKException = &code
+		m.SDKExceptionMessage = &msg
+	default:
+		m.AWSException = &code
+		m.AWSExceptionMessage = &msg
+	}
+}
+
+func (rep *Reporter) sendAPICallMetric(r *request.Request) {
+	if rep == nil {
+		return
+	}
+
+	now := time.Now()
+	m := metric{
+		ClientID:      aws.String(rep.clientID),
+		API:           aws.String(r.Operation.Name),
+		Service:       aws.String(r.ClientInfo.ServiceID),
+		Timestamp:     (*metricTime)(&now),
+		Type:          aws.String("ApiCall"),
+		AttemptCount:  aws.Int(r.RetryCount + 1),
+		Latency:       aws.Int(int(time.Now().Sub(r.Time) / time.Millisecond)),
+		XAmzRequestID: aws.String(r.RequestID),
+	}
+
+	// TODO: Probably want to figure something out for logging dropped
+	// metrics
+	rep.metricsCh.Push(m)
+}
+
+func (rep *Reporter) connect(network, url string) error {
+	if rep.conn != nil {
+		rep.conn.Close()
+	}
+
+	conn, err := net.Dial(network, url)
+	if err != nil {
+		return awserr.New("UDPError", "Could not connect", err)
+	}
+
+	rep.conn = conn
+
+	return nil
+}
+
+func (rep *Reporter) close() {
+	if rep.done != nil {
+		close(rep.done)
+	}
+
+	rep.metricsCh.Pause()
+}
+
+func (rep *Reporter) start() {
+	defer func() {
+		rep.metricsCh.Pause()
+	}()
+
+	for {
+		select {
+		case <-rep.done:
+			rep.done = nil
+			return
+		case m := <-rep.metricsCh.ch:
+			// TODO: What to do with this error? Probably should just log
+			b, err := json.Marshal(m)
+			if err != nil {
+				continue
+			}
+
+			rep.conn.Write(b)
+		}
+	}
+}
+
+// Pause will pause the metric channel preventing any new metrics from
+// being added.
+func (rep *Reporter) Pause() {
+	lock.Lock()
+	defer lock.Unlock()
+
+	if rep == nil {
+		return
+	}
+
+	rep.close()
+}
+
+// Continue will reopen the metric channel and allow for monitoring
+// to be resumed.
+func (rep *Reporter) Continue() {
+	lock.Lock()
+	defer lock.Unlock()
+	if rep == nil {
+		return
+	}
+
+	if !rep.metricsCh.IsPaused() {
+		return
+	}
+
+	rep.metricsCh.Continue()
+}
+
+// InjectHandlers will will enable client side metrics and inject the proper
+// handlers to handle how metrics are sent.
+//
+//	Example:
+//		// Start must be called in order to inject the correct handlers
+//		r, err := csm.Start("clientID", "127.0.0.1:8094")
+//		if err != nil {
+//			panic(fmt.Errorf("expected no error, but received %v", err))
+//		}
+//
+//		sess := session.NewSession()
+//		r.InjectHandlers(&sess.Handlers)
+//
+//		// create a new service client with our client side metric session
+//		svc := s3.New(sess)
+func (rep *Reporter) InjectHandlers(handlers *request.Handlers) {
+	if rep == nil {
+		return
+	}
+
+	apiCallHandler := request.NamedHandler{Name: APICallMetricHandlerName, Fn: rep.sendAPICallMetric}
+	handlers.Complete.PushFrontNamed(apiCallHandler)
+
+	apiCallAttemptHandler := request.NamedHandler{Name: APICallAttemptMetricHandlerName, Fn: rep.sendAPICallAttemptMetric}
+	handlers.AfterRetry.PushFrontNamed(apiCallAttemptHandler)
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go b/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go
index 857f677dd10..c472a57fad2 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go
@@ -48,6 +48,7 @@ const (
 	A4bServiceID                          = "a4b"                          // A4b.
 	AcmServiceID                          = "acm"                          // Acm.
 	AcmPcaServiceID                       = "acm-pca"                      // AcmPca.
+	ApiMediatailorServiceID               = "api.mediatailor"              // ApiMediatailor.
 	ApiPricingServiceID                   = "api.pricing"                  // ApiPricing.
 	ApigatewayServiceID                   = "apigateway"                   // Apigateway.
 	ApplicationAutoscalingServiceID       = "application-autoscaling"      // ApplicationAutoscaling.
@@ -130,6 +131,7 @@ const (
 	ModelsLexServiceID                    = "models.lex"                   // ModelsLex.
 	MonitoringServiceID                   = "monitoring"                   // Monitoring.
 	MturkRequesterServiceID               = "mturk-requester"              // MturkRequester.
+	NeptuneServiceID                      = "neptune"                      // Neptune.
 	OpsworksServiceID                     = "opsworks"                     // Opsworks.
 	OpsworksCmServiceID                   = "opsworks-cm"                  // OpsworksCm.
 	OrganizationsServiceID                = "organizations"                // Organizations.
@@ -307,6 +309,16 @@ var awsPartition = partition{
 				"us-west-2":      endpoint{},
 			},
 		},
+		"api.mediatailor": service{
+
+			Endpoints: endpoints{
+				"ap-northeast-1": endpoint{},
+				"ap-southeast-1": endpoint{},
+				"ap-southeast-2": endpoint{},
+				"eu-west-1":      endpoint{},
+				"us-east-1":      endpoint{},
+			},
+		},
 		"api.pricing": service{
 			Defaults: endpoint{
 				CredentialScope: credentialScope{
@@ -434,6 +446,7 @@ var awsPartition = partition{
 			Endpoints: endpoints{
 				"ap-northeast-1": endpoint{},
 				"ap-northeast-2": endpoint{},
+				"ap-south-1":     endpoint{},
 				"ap-southeast-1": endpoint{},
 				"ap-southeast-2": endpoint{},
 				"ca-central-1":   endpoint{},
@@ -1046,6 +1059,7 @@ var awsPartition = partition{
 		"elasticfilesystem": service{
 
 			Endpoints: endpoints{
+				"ap-northeast-2": endpoint{},
 				"ap-southeast-2": endpoint{},
 				"eu-central-1":   endpoint{},
 				"eu-west-1":      endpoint{},
@@ -1242,11 +1256,13 @@ var awsPartition = partition{
 
 			Endpoints: endpoints{
 				"ap-northeast-1": endpoint{},
+				"ap-northeast-2": endpoint{},
 				"ap-south-1":     endpoint{},
 				"ap-southeast-1": endpoint{},
 				"ap-southeast-2": endpoint{},
 				"eu-central-1":   endpoint{},
 				"eu-west-1":      endpoint{},
+				"eu-west-2":      endpoint{},
 				"us-east-1":      endpoint{},
 				"us-east-2":      endpoint{},
 				"us-west-2":      endpoint{},
@@ -1509,8 +1525,10 @@ var awsPartition = partition{
 
 			Endpoints: endpoints{
 				"ap-northeast-1": endpoint{},
+				"ap-northeast-2": endpoint{},
 				"ap-southeast-1": endpoint{},
 				"ap-southeast-2": endpoint{},
+				"eu-central-1":   endpoint{},
 				"eu-west-1":      endpoint{},
 				"us-east-1":      endpoint{},
 				"us-west-2":      endpoint{},
@@ -1622,6 +1640,35 @@ var awsPartition = partition{
 				"us-east-1": endpoint{},
 			},
 		},
+		"neptune": service{
+
+			Endpoints: endpoints{
+				"eu-west-1": endpoint{
+					Hostname: "rds.eu-west-1.amazonaws.com",
+					CredentialScope: credentialScope{
+						Region: "eu-west-1",
+					},
+				},
+				"us-east-1": endpoint{
+					Hostname: "rds.us-east-1.amazonaws.com",
+					CredentialScope: credentialScope{
+						Region: "us-east-1",
+					},
+				},
+				"us-east-2": endpoint{
+					Hostname: "rds.us-east-2.amazonaws.com",
+					CredentialScope: credentialScope{
+						Region: "us-east-2",
+					},
+				},
+				"us-west-2": endpoint{
+					Hostname: "rds.us-west-2.amazonaws.com",
+					CredentialScope: credentialScope{
+						Region: "us-west-2",
+					},
+				},
+			},
+		},
 		"opsworks": service{
 
 			Endpoints: endpoints{
@@ -1805,10 +1852,11 @@ var awsPartition = partition{
 		"runtime.sagemaker": service{
 
 			Endpoints: endpoints{
-				"eu-west-1": endpoint{},
-				"us-east-1": endpoint{},
-				"us-east-2": endpoint{},
-				"us-west-2": endpoint{},
+				"ap-northeast-1": endpoint{},
+				"eu-west-1":      endpoint{},
+				"us-east-1":      endpoint{},
+				"us-east-2":      endpoint{},
+				"us-west-2":      endpoint{},
 			},
 		},
 		"s3": service{
@@ -1873,10 +1921,11 @@ var awsPartition = partition{
 		"sagemaker": service{
 
 			Endpoints: endpoints{
-				"eu-west-1": endpoint{},
-				"us-east-1": endpoint{},
-				"us-east-2": endpoint{},
-				"us-west-2": endpoint{},
+				"ap-northeast-1": endpoint{},
+				"eu-west-1":      endpoint{},
+				"us-east-1":      endpoint{},
+				"us-east-2":      endpoint{},
+				"us-west-2":      endpoint{},
 			},
 		},
 		"sdb": service{
@@ -2081,6 +2130,10 @@ var awsPartition = partition{
 				"eu-west-1":      endpoint{},
 				"eu-west-2":      endpoint{},
 				"eu-west-3":      endpoint{},
+				"fips-us-east-1": endpoint{},
+				"fips-us-east-2": endpoint{},
+				"fips-us-west-1": endpoint{},
+				"fips-us-west-2": endpoint{},
 				"sa-east-1":      endpoint{},
 				"us-east-1": endpoint{
 					SSLCommonName: "queue.{dnsSuffix}",
@@ -2507,13 +2560,15 @@ var awscnPartition = partition{
 		"ecr": service{
 
 			Endpoints: endpoints{
-				"cn-north-1": endpoint{},
+				"cn-north-1":     endpoint{},
+				"cn-northwest-1": endpoint{},
 			},
 		},
 		"ecs": service{
 
 			Endpoints: endpoints{
-				"cn-north-1": endpoint{},
+				"cn-north-1":     endpoint{},
+				"cn-northwest-1": endpoint{},
 			},
 		},
 		"elasticache": service{
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/logger.go b/vendor/github.com/aws/aws-sdk-go/aws/logger.go
index 3babb5abdb6..6ed15b2ecc2 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/logger.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/logger.go
@@ -71,6 +71,12 @@ const (
 	// LogDebugWithRequestErrors states the SDK should log when service requests fail
 	// to build, send, validate, or unmarshal.
 	LogDebugWithRequestErrors
+
+	// LogDebugWithEventStreamBody states the SDK should log EventStream
+	// request and response bodys. This should be used to log the EventStream
+	// wire unmarshaled message content of requests and responses made while
+	// using the SDK Will also enable LogDebug.
+	LogDebugWithEventStreamBody
 )
 
 // A Logger is a minimalistic interface for the SDK to log messages to. Should
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/request/handlers.go b/vendor/github.com/aws/aws-sdk-go/aws/request/handlers.go
index 802ac88ad5c..605a72d3c94 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/request/handlers.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/request/handlers.go
@@ -14,6 +14,7 @@ type Handlers struct {
 	Send             HandlerList
 	ValidateResponse HandlerList
 	Unmarshal        HandlerList
+	UnmarshalStream  HandlerList
 	UnmarshalMeta    HandlerList
 	UnmarshalError   HandlerList
 	Retry            HandlerList
@@ -30,6 +31,7 @@ func (h *Handlers) Copy() Handlers {
 		Send:             h.Send.copy(),
 		ValidateResponse: h.ValidateResponse.copy(),
 		Unmarshal:        h.Unmarshal.copy(),
+		UnmarshalStream:  h.UnmarshalStream.copy(),
 		UnmarshalError:   h.UnmarshalError.copy(),
 		UnmarshalMeta:    h.UnmarshalMeta.copy(),
 		Retry:            h.Retry.copy(),
@@ -45,6 +47,7 @@ func (h *Handlers) Clear() {
 	h.Send.Clear()
 	h.Sign.Clear()
 	h.Unmarshal.Clear()
+	h.UnmarshalStream.Clear()
 	h.UnmarshalMeta.Clear()
 	h.UnmarshalError.Clear()
 	h.ValidateResponse.Clear()
@@ -172,6 +175,21 @@ func (l *HandlerList) SwapNamed(n NamedHandler) (swapped bool) {
 	return swapped
 }
 
+// Swap will swap out all handlers matching the name passed in. The matched
+// handlers will be swapped in. True is returned if the handlers were swapped.
+func (l *HandlerList) Swap(name string, replace NamedHandler) bool {
+	var swapped bool
+
+	for i := 0; i < len(l.list); i++ {
+		if l.list[i].Name == name {
+			l.list[i] = replace
+			swapped = true
+		}
+	}
+
+	return swapped
+}
+
 // SetBackNamed will replace the named handler if it exists in the handler list.
 // If the handler does not exist the handler will be added to the end of the list.
 func (l *HandlerList) SetBackNamed(n NamedHandler) {
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/request/request.go b/vendor/github.com/aws/aws-sdk-go/aws/request/request.go
index 69b7a01ad74..75f0fe07780 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/request/request.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/request/request.go
@@ -46,6 +46,7 @@ type Request struct {
 	Handlers   Handlers
 
 	Retryer
+	AttemptTime            time.Time
 	Time                   time.Time
 	Operation              *Operation
 	HTTPRequest            *http.Request
@@ -121,6 +122,7 @@ func New(cfg aws.Config, clientInfo metadata.ClientInfo, handlers Handlers,
 		Handlers:   handlers.Copy(),
 
 		Retryer:     retryer,
+		AttemptTime: time.Now(),
 		Time:        time.Now(),
 		ExpireTime:  0,
 		Operation:   operation,
@@ -368,9 +370,9 @@ func (r *Request) Build() error {
 	return r.Error
 }
 
-// Sign will sign the request returning error if errors are encountered.
+// Sign will sign the request, returning error if errors are encountered.
 //
-// Send will build the request prior to signing. All Sign Handlers will
+// Sign will build the request prior to signing. All Sign Handlers will
 // be executed in the order they were set.
 func (r *Request) Sign() error {
 	r.Build()
@@ -440,7 +442,7 @@ func (r *Request) GetBody() io.ReadSeeker {
 	return r.safeBody
 }
 
-// Send will send the request returning error if errors are encountered.
+// Send will send the request, returning error if errors are encountered.
 //
 // Send will sign the request prior to sending. All Send Handlers will
 // be executed in the order they were set.
@@ -461,6 +463,7 @@ func (r *Request) Send() error {
 	}()
 
 	for {
+		r.AttemptTime = time.Now()
 		if aws.BoolValue(r.Retryable) {
 			if r.Config.LogLevel.Matches(aws.LogDebugWithRequestRetries) {
 				r.Config.Logger.Log(fmt.Sprintf("DEBUG: Retrying Request %s/%s, attempt %d",
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/request/request_1_7.go b/vendor/github.com/aws/aws-sdk-go/aws/request/request_1_7.go
index 869b97a1a0f..e36e468b7c6 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/request/request_1_7.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/request/request_1_7.go
@@ -21,7 +21,7 @@ func (noBody) WriteTo(io.Writer) (int64, error) { return 0, nil }
 var NoBody = noBody{}
 
 // ResetBody rewinds the request body back to its starting position, and
-// set's the HTTP Request body reference. When the body is read prior
+// sets the HTTP Request body reference. When the body is read prior
 // to being sent in the HTTP request it will need to be rewound.
 //
 // ResetBody will automatically be called by the SDK's build handler, but if
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/request/request_1_8.go b/vendor/github.com/aws/aws-sdk-go/aws/request/request_1_8.go
index c32fc69bc56..7c6a8000f67 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/request/request_1_8.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/request/request_1_8.go
@@ -11,7 +11,7 @@ import (
 var NoBody = http.NoBody
 
 // ResetBody rewinds the request body back to its starting position, and
-// set's the HTTP Request body reference. When the body is read prior
+// sets the HTTP Request body reference. When the body is read prior
 // to being sent in the HTTP request it will need to be rewound.
 //
 // ResetBody will automatically be called by the SDK's build handler, but if
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/request/request_pagination.go b/vendor/github.com/aws/aws-sdk-go/aws/request/request_pagination.go
index 159518a75cd..a633ed5acfa 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/request/request_pagination.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/request/request_pagination.go
@@ -35,8 +35,12 @@ type Pagination struct {
 	// NewRequest should always be built from the same API operations. It is
 	// undefined if different API operations are returned on subsequent calls.
 	NewRequest func() (*Request, error)
+	// EndPageOnSameToken, when enabled, will allow the paginator to stop on
+	// token that are the same as its previous tokens.
+	EndPageOnSameToken bool
 
 	started    bool
+	prevTokens []interface{}
 	nextTokens []interface{}
 
 	err     error
@@ -49,7 +53,15 @@ type Pagination struct {
 //
 // Will always return true if Next has not been called yet.
 func (p *Pagination) HasNextPage() bool {
-	return !(p.started && len(p.nextTokens) == 0)
+	if !p.started {
+		return true
+	}
+
+	hasNextPage := len(p.nextTokens) != 0
+	if p.EndPageOnSameToken {
+		return hasNextPage && !awsutil.DeepEqual(p.nextTokens, p.prevTokens)
+	}
+	return hasNextPage
 }
 
 // Err returns the error Pagination encountered when retrieving the next page.
@@ -96,6 +108,7 @@ func (p *Pagination) Next() bool {
 		return false
 	}
 
+	p.prevTokens = p.nextTokens
 	p.nextTokens = req.nextPageTokens()
 	p.curPage = req.Data
 
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/session/env_config.go b/vendor/github.com/aws/aws-sdk-go/aws/session/env_config.go
index 12b452177a8..82e04d76cde 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/session/env_config.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/session/env_config.go
@@ -96,9 +96,23 @@ type envConfig struct {
 	//
 	//  AWS_CA_BUNDLE=$HOME/my_custom_ca_bundle
 	CustomCABundle string
+
+	csmEnabled  string
+	CSMEnabled  bool
+	CSMPort     string
+	CSMClientID string
 }
 
 var (
+	csmEnabledEnvKey = []string{
+		"AWS_CSM_ENABLED",
+	}
+	csmPortEnvKey = []string{
+		"AWS_CSM_PORT",
+	}
+	csmClientIDEnvKey = []string{
+		"AWS_CSM_CLIENT_ID",
+	}
 	credAccessEnvKey = []string{
 		"AWS_ACCESS_KEY_ID",
 		"AWS_ACCESS_KEY",
@@ -157,6 +171,12 @@ func envConfigLoad(enableSharedConfig bool) envConfig {
 	setFromEnvVal(&cfg.Creds.SecretAccessKey, credSecretEnvKey)
 	setFromEnvVal(&cfg.Creds.SessionToken, credSessionEnvKey)
 
+	// CSM environment variables
+	setFromEnvVal(&cfg.csmEnabled, csmEnabledEnvKey)
+	setFromEnvVal(&cfg.CSMPort, csmPortEnvKey)
+	setFromEnvVal(&cfg.CSMClientID, csmClientIDEnvKey)
+	cfg.CSMEnabled = len(cfg.csmEnabled) > 0
+
 	// Require logical grouping of credentials
 	if len(cfg.Creds.AccessKeyID) == 0 || len(cfg.Creds.SecretAccessKey) == 0 {
 		cfg.Creds = credentials.Value{}
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/session/session.go b/vendor/github.com/aws/aws-sdk-go/aws/session/session.go
index 259b5c0fecc..51f30556301 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/session/session.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/session/session.go
@@ -15,6 +15,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/corehandlers"
 	"github.com/aws/aws-sdk-go/aws/credentials"
 	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
+	"github.com/aws/aws-sdk-go/aws/csm"
 	"github.com/aws/aws-sdk-go/aws/defaults"
 	"github.com/aws/aws-sdk-go/aws/endpoints"
 	"github.com/aws/aws-sdk-go/aws/request"
@@ -81,10 +82,16 @@ func New(cfgs ...*aws.Config) *Session {
 				r.Error = err
 			})
 		}
+
 		return s
 	}
 
-	return deprecatedNewSession(cfgs...)
+	s := deprecatedNewSession(cfgs...)
+	if envCfg.CSMEnabled {
+		enableCSM(&s.Handlers, envCfg.CSMClientID, envCfg.CSMPort, s.Config.Logger)
+	}
+
+	return s
 }
 
 // NewSession returns a new Session created from SDK defaults, config files,
@@ -300,10 +307,22 @@ func deprecatedNewSession(cfgs ...*aws.Config) *Session {
 	}
 
 	initHandlers(s)
-
 	return s
 }
 
+func enableCSM(handlers *request.Handlers, clientID string, port string, logger aws.Logger) {
+	logger.Log("Enabling CSM")
+	if len(port) == 0 {
+		port = csm.DefaultPort
+	}
+
+	r, err := csm.Start(clientID, "127.0.0.1:"+port)
+	if err != nil {
+		return
+	}
+	r.InjectHandlers(handlers)
+}
+
 func newSession(opts Options, envCfg envConfig, cfgs ...*aws.Config) (*Session, error) {
 	cfg := defaults.Config()
 	handlers := defaults.Handlers()
@@ -343,6 +362,9 @@ func newSession(opts Options, envCfg envConfig, cfgs ...*aws.Config) (*Session,
 	}
 
 	initHandlers(s)
+	if envCfg.CSMEnabled {
+		enableCSM(&s.Handlers, envCfg.CSMClientID, envCfg.CSMPort, s.Config.Logger)
+	}
 
 	// Setup HTTP client with custom cert bundle if enabled
 	if opts.CustomCABundle != nil {
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/signer/v4/v4.go b/vendor/github.com/aws/aws-sdk-go/aws/signer/v4/v4.go
index 6e46376125b..f3586131538 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/signer/v4/v4.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/signer/v4/v4.go
@@ -135,6 +135,7 @@ var requiredSignedHeaders = rules{
 			"X-Amz-Server-Side-Encryption-Customer-Key-Md5":               struct{}{},
 			"X-Amz-Storage-Class":                                         struct{}{},
 			"X-Amz-Website-Redirect-Location":                             struct{}{},
+			"X-Amz-Content-Sha256":                                        struct{}{},
 		},
 	},
 	patterns{"X-Amz-Meta-"},
@@ -671,8 +672,15 @@ func (ctx *signingCtx) buildSignature() {
 func (ctx *signingCtx) buildBodyDigest() error {
 	hash := ctx.Request.Header.Get("X-Amz-Content-Sha256")
 	if hash == "" {
-		if ctx.unsignedPayload || (ctx.isPresign && ctx.ServiceName == "s3") {
+		includeSHA256Header := ctx.unsignedPayload ||
+			ctx.ServiceName == "s3" ||
+			ctx.ServiceName == "glacier"
+
+		s3Presign := ctx.isPresign && ctx.ServiceName == "s3"
+
+		if ctx.unsignedPayload || s3Presign {
 			hash = "UNSIGNED-PAYLOAD"
+			includeSHA256Header = !s3Presign
 		} else if ctx.Body == nil {
 			hash = emptyStringSHA256
 		} else {
@@ -681,7 +689,8 @@ func (ctx *signingCtx) buildBodyDigest() error {
 			}
 			hash = hex.EncodeToString(makeSha256Reader(ctx.Body))
 		}
-		if ctx.unsignedPayload || ctx.ServiceName == "s3" || ctx.ServiceName == "glacier" {
+
+		if includeSHA256Header {
 			ctx.Request.Header.Set("X-Amz-Content-Sha256", hash)
 		}
 	}
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/version.go b/vendor/github.com/aws/aws-sdk-go/aws/version.go
index befbff7df07..c108466609e 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/version.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/version.go
@@ -5,4 +5,4 @@ package aws
 const SDKName = "aws-sdk-go"
 
 // SDKVersion is the version of this SDK
-const SDKVersion = "1.13.49"
+const SDKVersion = "1.14.12"
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/debug.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/debug.go
new file mode 100644
index 00000000000..ecc7bf82fa2
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/debug.go
@@ -0,0 +1,144 @@
+package eventstream
+
+import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"strconv"
+)
+
+type decodedMessage struct {
+	rawMessage
+	Headers decodedHeaders `json:"headers"`
+}
+type jsonMessage struct {
+	Length     json.Number    `json:"total_length"`
+	HeadersLen json.Number    `json:"headers_length"`
+	PreludeCRC json.Number    `json:"prelude_crc"`
+	Headers    decodedHeaders `json:"headers"`
+	Payload    []byte         `json:"payload"`
+	CRC        json.Number    `json:"message_crc"`
+}
+
+func (d *decodedMessage) UnmarshalJSON(b []byte) (err error) {
+	var jsonMsg jsonMessage
+	if err = json.Unmarshal(b, &jsonMsg); err != nil {
+		return err
+	}
+
+	d.Length, err = numAsUint32(jsonMsg.Length)
+	if err != nil {
+		return err
+	}
+	d.HeadersLen, err = numAsUint32(jsonMsg.HeadersLen)
+	if err != nil {
+		return err
+	}
+	d.PreludeCRC, err = numAsUint32(jsonMsg.PreludeCRC)
+	if err != nil {
+		return err
+	}
+	d.Headers = jsonMsg.Headers
+	d.Payload = jsonMsg.Payload
+	d.CRC, err = numAsUint32(jsonMsg.CRC)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (d *decodedMessage) MarshalJSON() ([]byte, error) {
+	jsonMsg := jsonMessage{
+		Length:     json.Number(strconv.Itoa(int(d.Length))),
+		HeadersLen: json.Number(strconv.Itoa(int(d.HeadersLen))),
+		PreludeCRC: json.Number(strconv.Itoa(int(d.PreludeCRC))),
+		Headers:    d.Headers,
+		Payload:    d.Payload,
+		CRC:        json.Number(strconv.Itoa(int(d.CRC))),
+	}
+
+	return json.Marshal(jsonMsg)
+}
+
+func numAsUint32(n json.Number) (uint32, error) {
+	v, err := n.Int64()
+	if err != nil {
+		return 0, fmt.Errorf("failed to get int64 json number, %v", err)
+	}
+
+	return uint32(v), nil
+}
+
+func (d decodedMessage) Message() Message {
+	return Message{
+		Headers: Headers(d.Headers),
+		Payload: d.Payload,
+	}
+}
+
+type decodedHeaders Headers
+
+func (hs *decodedHeaders) UnmarshalJSON(b []byte) error {
+	var jsonHeaders []struct {
+		Name  string      `json:"name"`
+		Type  valueType   `json:"type"`
+		Value interface{} `json:"value"`
+	}
+
+	decoder := json.NewDecoder(bytes.NewReader(b))
+	decoder.UseNumber()
+	if err := decoder.Decode(&jsonHeaders); err != nil {
+		return err
+	}
+
+	var headers Headers
+	for _, h := range jsonHeaders {
+		value, err := valueFromType(h.Type, h.Value)
+		if err != nil {
+			return err
+		}
+		headers.Set(h.Name, value)
+	}
+	(*hs) = decodedHeaders(headers)
+
+	return nil
+}
+
+func valueFromType(typ valueType, val interface{}) (Value, error) {
+	switch typ {
+	case trueValueType:
+		return BoolValue(true), nil
+	case falseValueType:
+		return BoolValue(false), nil
+	case int8ValueType:
+		v, err := val.(json.Number).Int64()
+		return Int8Value(int8(v)), err
+	case int16ValueType:
+		v, err := val.(json.Number).Int64()
+		return Int16Value(int16(v)), err
+	case int32ValueType:
+		v, err := val.(json.Number).Int64()
+		return Int32Value(int32(v)), err
+	case int64ValueType:
+		v, err := val.(json.Number).Int64()
+		return Int64Value(v), err
+	case bytesValueType:
+		v, err := base64.StdEncoding.DecodeString(val.(string))
+		return BytesValue(v), err
+	case stringValueType:
+		v, err := base64.StdEncoding.DecodeString(val.(string))
+		return StringValue(string(v)), err
+	case timestampValueType:
+		v, err := val.(json.Number).Int64()
+		return TimestampValue(timeFromEpochMilli(v)), err
+	case uuidValueType:
+		v, err := base64.StdEncoding.DecodeString(val.(string))
+		var tv UUIDValue
+		copy(tv[:], v)
+		return tv, err
+	default:
+		panic(fmt.Sprintf("unknown type, %s, %T", typ.String(), val))
+	}
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/decode.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/decode.go
new file mode 100644
index 00000000000..4b972b2d666
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/decode.go
@@ -0,0 +1,199 @@
+package eventstream
+
+import (
+	"bytes"
+	"encoding/binary"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"hash"
+	"hash/crc32"
+	"io"
+
+	"github.com/aws/aws-sdk-go/aws"
+)
+
+// Decoder provides decoding of an Event Stream messages.
+type Decoder struct {
+	r      io.Reader
+	logger aws.Logger
+}
+
+// NewDecoder initializes and returns a Decoder for decoding event
+// stream messages from the reader provided.
+func NewDecoder(r io.Reader) *Decoder {
+	return &Decoder{
+		r: r,
+	}
+}
+
+// Decode attempts to decode a single message from the event stream reader.
+// Will return the event stream message, or error if Decode fails to read
+// the message from the stream.
+func (d *Decoder) Decode(payloadBuf []byte) (m Message, err error) {
+	reader := d.r
+	if d.logger != nil {
+		debugMsgBuf := bytes.NewBuffer(nil)
+		reader = io.TeeReader(reader, debugMsgBuf)
+		defer func() {
+			logMessageDecode(d.logger, debugMsgBuf, m, err)
+		}()
+	}
+
+	crc := crc32.New(crc32IEEETable)
+	hashReader := io.TeeReader(reader, crc)
+
+	prelude, err := decodePrelude(hashReader, crc)
+	if err != nil {
+		return Message{}, err
+	}
+
+	if prelude.HeadersLen > 0 {
+		lr := io.LimitReader(hashReader, int64(prelude.HeadersLen))
+		m.Headers, err = decodeHeaders(lr)
+		if err != nil {
+			return Message{}, err
+		}
+	}
+
+	if payloadLen := prelude.PayloadLen(); payloadLen > 0 {
+		buf, err := decodePayload(payloadBuf, io.LimitReader(hashReader, int64(payloadLen)))
+		if err != nil {
+			return Message{}, err
+		}
+		m.Payload = buf
+	}
+
+	msgCRC := crc.Sum32()
+	if err := validateCRC(reader, msgCRC); err != nil {
+		return Message{}, err
+	}
+
+	return m, nil
+}
+
+// UseLogger specifies the Logger that that the decoder should use to log the
+// message decode to.
+func (d *Decoder) UseLogger(logger aws.Logger) {
+	d.logger = logger
+}
+
+func logMessageDecode(logger aws.Logger, msgBuf *bytes.Buffer, msg Message, decodeErr error) {
+	w := bytes.NewBuffer(nil)
+	defer func() { logger.Log(w.String()) }()
+
+	fmt.Fprintf(w, "Raw message:\n%s\n",
+		hex.Dump(msgBuf.Bytes()))
+
+	if decodeErr != nil {
+		fmt.Fprintf(w, "Decode error: %v\n", decodeErr)
+		return
+	}
+
+	rawMsg, err := msg.rawMessage()
+	if err != nil {
+		fmt.Fprintf(w, "failed to create raw message, %v\n", err)
+		return
+	}
+
+	decodedMsg := decodedMessage{
+		rawMessage: rawMsg,
+		Headers:    decodedHeaders(msg.Headers),
+	}
+
+	fmt.Fprintf(w, "Decoded message:\n")
+	encoder := json.NewEncoder(w)
+	if err := encoder.Encode(decodedMsg); err != nil {
+		fmt.Fprintf(w, "failed to generate decoded message, %v\n", err)
+	}
+}
+
+func decodePrelude(r io.Reader, crc hash.Hash32) (messagePrelude, error) {
+	var p messagePrelude
+
+	var err error
+	p.Length, err = decodeUint32(r)
+	if err != nil {
+		return messagePrelude{}, err
+	}
+
+	p.HeadersLen, err = decodeUint32(r)
+	if err != nil {
+		return messagePrelude{}, err
+	}
+
+	if err := p.ValidateLens(); err != nil {
+		return messagePrelude{}, err
+	}
+
+	preludeCRC := crc.Sum32()
+	if err := validateCRC(r, preludeCRC); err != nil {
+		return messagePrelude{}, err
+	}
+
+	p.PreludeCRC = preludeCRC
+
+	return p, nil
+}
+
+func decodePayload(buf []byte, r io.Reader) ([]byte, error) {
+	w := bytes.NewBuffer(buf[0:0])
+
+	_, err := io.Copy(w, r)
+	return w.Bytes(), err
+}
+
+func decodeUint8(r io.Reader) (uint8, error) {
+	type byteReader interface {
+		ReadByte() (byte, error)
+	}
+
+	if br, ok := r.(byteReader); ok {
+		v, err := br.ReadByte()
+		return uint8(v), err
+	}
+
+	var b [1]byte
+	_, err := io.ReadFull(r, b[:])
+	return uint8(b[0]), err
+}
+func decodeUint16(r io.Reader) (uint16, error) {
+	var b [2]byte
+	bs := b[:]
+	_, err := io.ReadFull(r, bs)
+	if err != nil {
+		return 0, err
+	}
+	return binary.BigEndian.Uint16(bs), nil
+}
+func decodeUint32(r io.Reader) (uint32, error) {
+	var b [4]byte
+	bs := b[:]
+	_, err := io.ReadFull(r, bs)
+	if err != nil {
+		return 0, err
+	}
+	return binary.BigEndian.Uint32(bs), nil
+}
+func decodeUint64(r io.Reader) (uint64, error) {
+	var b [8]byte
+	bs := b[:]
+	_, err := io.ReadFull(r, bs)
+	if err != nil {
+		return 0, err
+	}
+	return binary.BigEndian.Uint64(bs), nil
+}
+
+func validateCRC(r io.Reader, expect uint32) error {
+	msgCRC, err := decodeUint32(r)
+	if err != nil {
+		return err
+	}
+
+	if msgCRC != expect {
+		return ChecksumError{}
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/encode.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/encode.go
new file mode 100644
index 00000000000..150a60981d8
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/encode.go
@@ -0,0 +1,114 @@
+package eventstream
+
+import (
+	"bytes"
+	"encoding/binary"
+	"hash"
+	"hash/crc32"
+	"io"
+)
+
+// Encoder provides EventStream message encoding.
+type Encoder struct {
+	w io.Writer
+
+	headersBuf *bytes.Buffer
+}
+
+// NewEncoder initializes and returns an Encoder to encode Event Stream
+// messages to an io.Writer.
+func NewEncoder(w io.Writer) *Encoder {
+	return &Encoder{
+		w:          w,
+		headersBuf: bytes.NewBuffer(nil),
+	}
+}
+
+// Encode encodes a single EventStream message to the io.Writer the Encoder
+// was created with. An error is returned if writing the message fails.
+func (e *Encoder) Encode(msg Message) error {
+	e.headersBuf.Reset()
+
+	err := encodeHeaders(e.headersBuf, msg.Headers)
+	if err != nil {
+		return err
+	}
+
+	crc := crc32.New(crc32IEEETable)
+	hashWriter := io.MultiWriter(e.w, crc)
+
+	headersLen := uint32(e.headersBuf.Len())
+	payloadLen := uint32(len(msg.Payload))
+
+	if err := encodePrelude(hashWriter, crc, headersLen, payloadLen); err != nil {
+		return err
+	}
+
+	if headersLen > 0 {
+		if _, err := io.Copy(hashWriter, e.headersBuf); err != nil {
+			return err
+		}
+	}
+
+	if payloadLen > 0 {
+		if _, err := hashWriter.Write(msg.Payload); err != nil {
+			return err
+		}
+	}
+
+	msgCRC := crc.Sum32()
+	return binary.Write(e.w, binary.BigEndian, msgCRC)
+}
+
+func encodePrelude(w io.Writer, crc hash.Hash32, headersLen, payloadLen uint32) error {
+	p := messagePrelude{
+		Length:     minMsgLen + headersLen + payloadLen,
+		HeadersLen: headersLen,
+	}
+	if err := p.ValidateLens(); err != nil {
+		return err
+	}
+
+	err := binaryWriteFields(w, binary.BigEndian,
+		p.Length,
+		p.HeadersLen,
+	)
+	if err != nil {
+		return err
+	}
+
+	p.PreludeCRC = crc.Sum32()
+	err = binary.Write(w, binary.BigEndian, p.PreludeCRC)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func encodeHeaders(w io.Writer, headers Headers) error {
+	for _, h := range headers {
+		hn := headerName{
+			Len: uint8(len(h.Name)),
+		}
+		copy(hn.Name[:hn.Len], h.Name)
+		if err := hn.encode(w); err != nil {
+			return err
+		}
+
+		if err := h.Value.encode(w); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func binaryWriteFields(w io.Writer, order binary.ByteOrder, vs ...interface{}) error {
+	for _, v := range vs {
+		if err := binary.Write(w, order, v); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/error.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/error.go
new file mode 100644
index 00000000000..5481ef30796
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/error.go
@@ -0,0 +1,23 @@
+package eventstream
+
+import "fmt"
+
+// LengthError provides the error for items being larger than a maximum length.
+type LengthError struct {
+	Part  string
+	Want  int
+	Have  int
+	Value interface{}
+}
+
+func (e LengthError) Error() string {
+	return fmt.Sprintf("%s length invalid, %d/%d, %v",
+		e.Part, e.Want, e.Have, e.Value)
+}
+
+// ChecksumError provides the error for message checksum invalidation errors.
+type ChecksumError struct{}
+
+func (e ChecksumError) Error() string {
+	return "message checksum mismatch"
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi/api.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi/api.go
new file mode 100644
index 00000000000..4a4e64c713e
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi/api.go
@@ -0,0 +1,160 @@
+package eventstreamapi
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/private/protocol"
+	"github.com/aws/aws-sdk-go/private/protocol/eventstream"
+)
+
+// Unmarshaler provides the interface for unmarshaling a EventStream
+// message into a SDK type.
+type Unmarshaler interface {
+	UnmarshalEvent(protocol.PayloadUnmarshaler, eventstream.Message) error
+}
+
+// EventStream headers with specific meaning to async API functionality.
+const (
+	MessageTypeHeader    = `:message-type` // Identifies type of message.
+	EventMessageType     = `event`
+	ErrorMessageType     = `error`
+	ExceptionMessageType = `exception`
+
+	// Message Events
+	EventTypeHeader = `:event-type` // Identifies message event type e.g. "Stats".
+
+	// Message Error
+	ErrorCodeHeader    = `:error-code`
+	ErrorMessageHeader = `:error-message`
+
+	// Message Exception
+	ExceptionTypeHeader = `:exception-type`
+)
+
+// EventReader provides reading from the EventStream of an reader.
+type EventReader struct {
+	reader  io.ReadCloser
+	decoder *eventstream.Decoder
+
+	unmarshalerForEventType func(string) (Unmarshaler, error)
+	payloadUnmarshaler      protocol.PayloadUnmarshaler
+
+	payloadBuf []byte
+}
+
+// NewEventReader returns a EventReader built from the reader and unmarshaler
+// provided.  Use ReadStream method to start reading from the EventStream.
+func NewEventReader(
+	reader io.ReadCloser,
+	payloadUnmarshaler protocol.PayloadUnmarshaler,
+	unmarshalerForEventType func(string) (Unmarshaler, error),
+) *EventReader {
+	return &EventReader{
+		reader:                  reader,
+		decoder:                 eventstream.NewDecoder(reader),
+		payloadUnmarshaler:      payloadUnmarshaler,
+		unmarshalerForEventType: unmarshalerForEventType,
+		payloadBuf:              make([]byte, 10*1024),
+	}
+}
+
+// UseLogger instructs the EventReader to use the logger and log level
+// specified.
+func (r *EventReader) UseLogger(logger aws.Logger, logLevel aws.LogLevelType) {
+	if logger != nil && logLevel.Matches(aws.LogDebugWithEventStreamBody) {
+		r.decoder.UseLogger(logger)
+	}
+}
+
+// ReadEvent attempts to read a message from the EventStream and return the
+// unmarshaled event value that the message is for.
+//
+// For EventStream API errors check if the returned error satisfies the
+// awserr.Error interface to get the error's Code and Message components.
+//
+// EventUnmarshalers called with EventStream messages must take copies of the
+// message's Payload. The payload will is reused between events read.
+func (r *EventReader) ReadEvent() (event interface{}, err error) {
+	msg, err := r.decoder.Decode(r.payloadBuf)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		// Reclaim payload buffer for next message read.
+		r.payloadBuf = msg.Payload[0:0]
+	}()
+
+	typ, err := GetHeaderString(msg, MessageTypeHeader)
+	if err != nil {
+		return nil, err
+	}
+
+	switch typ {
+	case EventMessageType:
+		return r.unmarshalEventMessage(msg)
+	case ErrorMessageType:
+		return nil, r.unmarshalErrorMessage(msg)
+	default:
+		return nil, fmt.Errorf("unknown eventstream message type, %v", typ)
+	}
+}
+
+func (r *EventReader) unmarshalEventMessage(
+	msg eventstream.Message,
+) (event interface{}, err error) {
+	eventType, err := GetHeaderString(msg, EventTypeHeader)
+	if err != nil {
+		return nil, err
+	}
+
+	ev, err := r.unmarshalerForEventType(eventType)
+	if err != nil {
+		return nil, err
+	}
+
+	err = ev.UnmarshalEvent(r.payloadUnmarshaler, msg)
+	if err != nil {
+		return nil, err
+	}
+
+	return ev, nil
+}
+
+func (r *EventReader) unmarshalErrorMessage(msg eventstream.Message) (err error) {
+	var msgErr messageError
+
+	msgErr.code, err = GetHeaderString(msg, ErrorCodeHeader)
+	if err != nil {
+		return err
+	}
+
+	msgErr.msg, err = GetHeaderString(msg, ErrorMessageHeader)
+	if err != nil {
+		return err
+	}
+
+	return msgErr
+}
+
+// Close closes the EventReader's EventStream reader.
+func (r *EventReader) Close() error {
+	return r.reader.Close()
+}
+
+// GetHeaderString returns the value of the header as a string. If the header
+// is not set or the value is not a string an error will be returned.
+func GetHeaderString(msg eventstream.Message, headerName string) (string, error) {
+	headerVal := msg.Headers.Get(headerName)
+	if headerVal == nil {
+		return "", fmt.Errorf("error header %s not present", headerName)
+	}
+
+	v, ok := headerVal.Get().(string)
+	if !ok {
+		return "", fmt.Errorf("error header value is not a string, %T", headerVal)
+	}
+
+	return v, nil
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi/error.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi/error.go
new file mode 100644
index 00000000000..5ea5a988b63
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi/error.go
@@ -0,0 +1,24 @@
+package eventstreamapi
+
+import "fmt"
+
+type messageError struct {
+	code string
+	msg  string
+}
+
+func (e messageError) Code() string {
+	return e.code
+}
+
+func (e messageError) Message() string {
+	return e.msg
+}
+
+func (e messageError) Error() string {
+	return fmt.Sprintf("%s: %s", e.code, e.msg)
+}
+
+func (e messageError) OrigErr() error {
+	return nil
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/header.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/header.go
new file mode 100644
index 00000000000..3b44dde2f32
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/header.go
@@ -0,0 +1,166 @@
+package eventstream
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+)
+
+// Headers are a collection of EventStream header values.
+type Headers []Header
+
+// Header is a single EventStream Key Value header pair.
+type Header struct {
+	Name  string
+	Value Value
+}
+
+// Set associates the name with a value. If the header name already exists in
+// the Headers the value will be replaced with the new one.
+func (hs *Headers) Set(name string, value Value) {
+	var i int
+	for ; i < len(*hs); i++ {
+		if (*hs)[i].Name == name {
+			(*hs)[i].Value = value
+			return
+		}
+	}
+
+	*hs = append(*hs, Header{
+		Name: name, Value: value,
+	})
+}
+
+// Get returns the Value associated with the header. Nil is returned if the
+// value does not exist.
+func (hs Headers) Get(name string) Value {
+	for i := 0; i < len(hs); i++ {
+		if h := hs[i]; h.Name == name {
+			return h.Value
+		}
+	}
+	return nil
+}
+
+// Del deletes the value in the Headers if it exists.
+func (hs *Headers) Del(name string) {
+	for i := 0; i < len(*hs); i++ {
+		if (*hs)[i].Name == name {
+			copy((*hs)[i:], (*hs)[i+1:])
+			(*hs) = (*hs)[:len(*hs)-1]
+		}
+	}
+}
+
+func decodeHeaders(r io.Reader) (Headers, error) {
+	hs := Headers{}
+
+	for {
+		name, err := decodeHeaderName(r)
+		if err != nil {
+			if err == io.EOF {
+				// EOF while getting header name means no more headers
+				break
+			}
+			return nil, err
+		}
+
+		value, err := decodeHeaderValue(r)
+		if err != nil {
+			return nil, err
+		}
+
+		hs.Set(name, value)
+	}
+
+	return hs, nil
+}
+
+func decodeHeaderName(r io.Reader) (string, error) {
+	var n headerName
+
+	var err error
+	n.Len, err = decodeUint8(r)
+	if err != nil {
+		return "", err
+	}
+
+	name := n.Name[:n.Len]
+	if _, err := io.ReadFull(r, name); err != nil {
+		return "", err
+	}
+
+	return string(name), nil
+}
+
+func decodeHeaderValue(r io.Reader) (Value, error) {
+	var raw rawValue
+
+	typ, err := decodeUint8(r)
+	if err != nil {
+		return nil, err
+	}
+	raw.Type = valueType(typ)
+
+	var v Value
+
+	switch raw.Type {
+	case trueValueType:
+		v = BoolValue(true)
+	case falseValueType:
+		v = BoolValue(false)
+	case int8ValueType:
+		var tv Int8Value
+		err = tv.decode(r)
+		v = tv
+	case int16ValueType:
+		var tv Int16Value
+		err = tv.decode(r)
+		v = tv
+	case int32ValueType:
+		var tv Int32Value
+		err = tv.decode(r)
+		v = tv
+	case int64ValueType:
+		var tv Int64Value
+		err = tv.decode(r)
+		v = tv
+	case bytesValueType:
+		var tv BytesValue
+		err = tv.decode(r)
+		v = tv
+	case stringValueType:
+		var tv StringValue
+		err = tv.decode(r)
+		v = tv
+	case timestampValueType:
+		var tv TimestampValue
+		err = tv.decode(r)
+		v = tv
+	case uuidValueType:
+		var tv UUIDValue
+		err = tv.decode(r)
+		v = tv
+	default:
+		panic(fmt.Sprintf("unknown value type %d", raw.Type))
+	}
+
+	// Error could be EOF, let caller deal with it
+	return v, err
+}
+
+const maxHeaderNameLen = 255
+
+type headerName struct {
+	Len  uint8
+	Name [maxHeaderNameLen]byte
+}
+
+func (v headerName) encode(w io.Writer) error {
+	if err := binary.Write(w, binary.BigEndian, v.Len); err != nil {
+		return err
+	}
+
+	_, err := w.Write(v.Name[:v.Len])
+	return err
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/header_value.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/header_value.go
new file mode 100644
index 00000000000..d7786f92ce5
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/header_value.go
@@ -0,0 +1,501 @@
+package eventstream
+
+import (
+	"encoding/base64"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"strconv"
+	"time"
+)
+
+const maxHeaderValueLen = 1<<15 - 1 // 2^15-1 or 32KB - 1
+
+// valueType is the EventStream header value type.
+type valueType uint8
+
+// Header value types
+const (
+	trueValueType valueType = iota
+	falseValueType
+	int8ValueType  // Byte
+	int16ValueType // Short
+	int32ValueType // Integer
+	int64ValueType // Long
+	bytesValueType
+	stringValueType
+	timestampValueType
+	uuidValueType
+)
+
+func (t valueType) String() string {
+	switch t {
+	case trueValueType:
+		return "bool"
+	case falseValueType:
+		return "bool"
+	case int8ValueType:
+		return "int8"
+	case int16ValueType:
+		return "int16"
+	case int32ValueType:
+		return "int32"
+	case int64ValueType:
+		return "int64"
+	case bytesValueType:
+		return "byte_array"
+	case stringValueType:
+		return "string"
+	case timestampValueType:
+		return "timestamp"
+	case uuidValueType:
+		return "uuid"
+	default:
+		return fmt.Sprintf("unknown value type %d", uint8(t))
+	}
+}
+
+type rawValue struct {
+	Type  valueType
+	Len   uint16 // Only set for variable length slices
+	Value []byte // byte representation of value, BigEndian encoding.
+}
+
+func (r rawValue) encodeScalar(w io.Writer, v interface{}) error {
+	return binaryWriteFields(w, binary.BigEndian,
+		r.Type,
+		v,
+	)
+}
+
+func (r rawValue) encodeFixedSlice(w io.Writer, v []byte) error {
+	binary.Write(w, binary.BigEndian, r.Type)
+
+	_, err := w.Write(v)
+	return err
+}
+
+func (r rawValue) encodeBytes(w io.Writer, v []byte) error {
+	if len(v) > maxHeaderValueLen {
+		return LengthError{
+			Part: "header value",
+			Want: maxHeaderValueLen, Have: len(v),
+			Value: v,
+		}
+	}
+	r.Len = uint16(len(v))
+
+	err := binaryWriteFields(w, binary.BigEndian,
+		r.Type,
+		r.Len,
+	)
+	if err != nil {
+		return err
+	}
+
+	_, err = w.Write(v)
+	return err
+}
+
+func (r rawValue) encodeString(w io.Writer, v string) error {
+	if len(v) > maxHeaderValueLen {
+		return LengthError{
+			Part: "header value",
+			Want: maxHeaderValueLen, Have: len(v),
+			Value: v,
+		}
+	}
+	r.Len = uint16(len(v))
+
+	type stringWriter interface {
+		WriteString(string) (int, error)
+	}
+
+	err := binaryWriteFields(w, binary.BigEndian,
+		r.Type,
+		r.Len,
+	)
+	if err != nil {
+		return err
+	}
+
+	if sw, ok := w.(stringWriter); ok {
+		_, err = sw.WriteString(v)
+	} else {
+		_, err = w.Write([]byte(v))
+	}
+
+	return err
+}
+
+func decodeFixedBytesValue(r io.Reader, buf []byte) error {
+	_, err := io.ReadFull(r, buf)
+	return err
+}
+
+func decodeBytesValue(r io.Reader) ([]byte, error) {
+	var raw rawValue
+	var err error
+	raw.Len, err = decodeUint16(r)
+	if err != nil {
+		return nil, err
+	}
+
+	buf := make([]byte, raw.Len)
+	_, err = io.ReadFull(r, buf)
+	if err != nil {
+		return nil, err
+	}
+
+	return buf, nil
+}
+
+func decodeStringValue(r io.Reader) (string, error) {
+	v, err := decodeBytesValue(r)
+	return string(v), err
+}
+
+// Value represents the abstract header value.
+type Value interface {
+	Get() interface{}
+	String() string
+	valueType() valueType
+	encode(io.Writer) error
+}
+
+// An BoolValue provides eventstream encoding, and representation
+// of a Go bool value.
+type BoolValue bool
+
+// Get returns the underlying type
+func (v BoolValue) Get() interface{} {
+	return bool(v)
+}
+
+// valueType returns the EventStream header value type value.
+func (v BoolValue) valueType() valueType {
+	if v {
+		return trueValueType
+	}
+	return falseValueType
+}
+
+func (v BoolValue) String() string {
+	return strconv.FormatBool(bool(v))
+}
+
+// encode encodes the BoolValue into an eventstream binary value
+// representation.
+func (v BoolValue) encode(w io.Writer) error {
+	return binary.Write(w, binary.BigEndian, v.valueType())
+}
+
+// An Int8Value provides eventstream encoding, and representation of a Go
+// int8 value.
+type Int8Value int8
+
+// Get returns the underlying value.
+func (v Int8Value) Get() interface{} {
+	return int8(v)
+}
+
+// valueType returns the EventStream header value type value.
+func (Int8Value) valueType() valueType {
+	return int8ValueType
+}
+
+func (v Int8Value) String() string {
+	return fmt.Sprintf("0x%02x", int8(v))
+}
+
+// encode encodes the Int8Value into an eventstream binary value
+// representation.
+func (v Int8Value) encode(w io.Writer) error {
+	raw := rawValue{
+		Type: v.valueType(),
+	}
+
+	return raw.encodeScalar(w, v)
+}
+
+func (v *Int8Value) decode(r io.Reader) error {
+	n, err := decodeUint8(r)
+	if err != nil {
+		return err
+	}
+
+	*v = Int8Value(n)
+	return nil
+}
+
+// An Int16Value provides eventstream encoding, and representation of a Go
+// int16 value.
+type Int16Value int16
+
+// Get returns the underlying value.
+func (v Int16Value) Get() interface{} {
+	return int16(v)
+}
+
+// valueType returns the EventStream header value type value.
+func (Int16Value) valueType() valueType {
+	return int16ValueType
+}
+
+func (v Int16Value) String() string {
+	return fmt.Sprintf("0x%04x", int16(v))
+}
+
+// encode encodes the Int16Value into an eventstream binary value
+// representation.
+func (v Int16Value) encode(w io.Writer) error {
+	raw := rawValue{
+		Type: v.valueType(),
+	}
+	return raw.encodeScalar(w, v)
+}
+
+func (v *Int16Value) decode(r io.Reader) error {
+	n, err := decodeUint16(r)
+	if err != nil {
+		return err
+	}
+
+	*v = Int16Value(n)
+	return nil
+}
+
+// An Int32Value provides eventstream encoding, and representation of a Go
+// int32 value.
+type Int32Value int32
+
+// Get returns the underlying value.
+func (v Int32Value) Get() interface{} {
+	return int32(v)
+}
+
+// valueType returns the EventStream header value type value.
+func (Int32Value) valueType() valueType {
+	return int32ValueType
+}
+
+func (v Int32Value) String() string {
+	return fmt.Sprintf("0x%08x", int32(v))
+}
+
+// encode encodes the Int32Value into an eventstream binary value
+// representation.
+func (v Int32Value) encode(w io.Writer) error {
+	raw := rawValue{
+		Type: v.valueType(),
+	}
+	return raw.encodeScalar(w, v)
+}
+
+func (v *Int32Value) decode(r io.Reader) error {
+	n, err := decodeUint32(r)
+	if err != nil {
+		return err
+	}
+
+	*v = Int32Value(n)
+	return nil
+}
+
+// An Int64Value provides eventstream encoding, and representation of a Go
+// int64 value.
+type Int64Value int64
+
+// Get returns the underlying value.
+func (v Int64Value) Get() interface{} {
+	return int64(v)
+}
+
+// valueType returns the EventStream header value type value.
+func (Int64Value) valueType() valueType {
+	return int64ValueType
+}
+
+func (v Int64Value) String() string {
+	return fmt.Sprintf("0x%016x", int64(v))
+}
+
+// encode encodes the Int64Value into an eventstream binary value
+// representation.
+func (v Int64Value) encode(w io.Writer) error {
+	raw := rawValue{
+		Type: v.valueType(),
+	}
+	return raw.encodeScalar(w, v)
+}
+
+func (v *Int64Value) decode(r io.Reader) error {
+	n, err := decodeUint64(r)
+	if err != nil {
+		return err
+	}
+
+	*v = Int64Value(n)
+	return nil
+}
+
+// An BytesValue provides eventstream encoding, and representation of a Go
+// byte slice.
+type BytesValue []byte
+
+// Get returns the underlying value.
+func (v BytesValue) Get() interface{} {
+	return []byte(v)
+}
+
+// valueType returns the EventStream header value type value.
+func (BytesValue) valueType() valueType {
+	return bytesValueType
+}
+
+func (v BytesValue) String() string {
+	return base64.StdEncoding.EncodeToString([]byte(v))
+}
+
+// encode encodes the BytesValue into an eventstream binary value
+// representation.
+func (v BytesValue) encode(w io.Writer) error {
+	raw := rawValue{
+		Type: v.valueType(),
+	}
+
+	return raw.encodeBytes(w, []byte(v))
+}
+
+func (v *BytesValue) decode(r io.Reader) error {
+	buf, err := decodeBytesValue(r)
+	if err != nil {
+		return err
+	}
+
+	*v = BytesValue(buf)
+	return nil
+}
+
+// An StringValue provides eventstream encoding, and representation of a Go
+// string.
+type StringValue string
+
+// Get returns the underlying value.
+func (v StringValue) Get() interface{} {
+	return string(v)
+}
+
+// valueType returns the EventStream header value type value.
+func (StringValue) valueType() valueType {
+	return stringValueType
+}
+
+func (v StringValue) String() string {
+	return string(v)
+}
+
+// encode encodes the StringValue into an eventstream binary value
+// representation.
+func (v StringValue) encode(w io.Writer) error {
+	raw := rawValue{
+		Type: v.valueType(),
+	}
+
+	return raw.encodeString(w, string(v))
+}
+
+func (v *StringValue) decode(r io.Reader) error {
+	s, err := decodeStringValue(r)
+	if err != nil {
+		return err
+	}
+
+	*v = StringValue(s)
+	return nil
+}
+
+// An TimestampValue provides eventstream encoding, and representation of a Go
+// timestamp.
+type TimestampValue time.Time
+
+// Get returns the underlying value.
+func (v TimestampValue) Get() interface{} {
+	return time.Time(v)
+}
+
+// valueType returns the EventStream header value type value.
+func (TimestampValue) valueType() valueType {
+	return timestampValueType
+}
+
+func (v TimestampValue) epochMilli() int64 {
+	nano := time.Time(v).UnixNano()
+	msec := nano / int64(time.Millisecond)
+	return msec
+}
+
+func (v TimestampValue) String() string {
+	msec := v.epochMilli()
+	return strconv.FormatInt(msec, 10)
+}
+
+// encode encodes the TimestampValue into an eventstream binary value
+// representation.
+func (v TimestampValue) encode(w io.Writer) error {
+	raw := rawValue{
+		Type: v.valueType(),
+	}
+
+	msec := v.epochMilli()
+	return raw.encodeScalar(w, msec)
+}
+
+func (v *TimestampValue) decode(r io.Reader) error {
+	n, err := decodeUint64(r)
+	if err != nil {
+		return err
+	}
+
+	*v = TimestampValue(timeFromEpochMilli(int64(n)))
+	return nil
+}
+
+func timeFromEpochMilli(t int64) time.Time {
+	secs := t / 1e3
+	msec := t % 1e3
+	return time.Unix(secs, msec*int64(time.Millisecond))
+}
+
+// An UUIDValue provides eventstream encoding, and representation of a UUID
+// value.
+type UUIDValue [16]byte
+
+// Get returns the underlying value.
+func (v UUIDValue) Get() interface{} {
+	return v[:]
+}
+
+// valueType returns the EventStream header value type value.
+func (UUIDValue) valueType() valueType {
+	return uuidValueType
+}
+
+func (v UUIDValue) String() string {
+	return fmt.Sprintf(`%X-%X-%X-%X-%X`, v[0:4], v[4:6], v[6:8], v[8:10], v[10:])
+}
+
+// encode encodes the UUIDValue into an eventstream binary value
+// representation.
+func (v UUIDValue) encode(w io.Writer) error {
+	raw := rawValue{
+		Type: v.valueType(),
+	}
+
+	return raw.encodeFixedSlice(w, v[:])
+}
+
+func (v *UUIDValue) decode(r io.Reader) error {
+	tv := (*v)[:]
+	return decodeFixedBytesValue(r, tv)
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/message.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/message.go
new file mode 100644
index 00000000000..2dc012a66e2
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/eventstream/message.go
@@ -0,0 +1,103 @@
+package eventstream
+
+import (
+	"bytes"
+	"encoding/binary"
+	"hash/crc32"
+)
+
+const preludeLen = 8
+const preludeCRCLen = 4
+const msgCRCLen = 4
+const minMsgLen = preludeLen + preludeCRCLen + msgCRCLen
+const maxPayloadLen = 1024 * 1024 * 16 // 16MB
+const maxHeadersLen = 1024 * 128       // 128KB
+const maxMsgLen = minMsgLen + maxHeadersLen + maxPayloadLen
+
+var crc32IEEETable = crc32.MakeTable(crc32.IEEE)
+
+// A Message provides the eventstream message representation.
+type Message struct {
+	Headers Headers
+	Payload []byte
+}
+
+func (m *Message) rawMessage() (rawMessage, error) {
+	var raw rawMessage
+
+	if len(m.Headers) > 0 {
+		var headers bytes.Buffer
+		if err := encodeHeaders(&headers, m.Headers); err != nil {
+			return rawMessage{}, err
+		}
+		raw.Headers = headers.Bytes()
+		raw.HeadersLen = uint32(len(raw.Headers))
+	}
+
+	raw.Length = raw.HeadersLen + uint32(len(m.Payload)) + minMsgLen
+
+	hash := crc32.New(crc32IEEETable)
+	binaryWriteFields(hash, binary.BigEndian, raw.Length, raw.HeadersLen)
+	raw.PreludeCRC = hash.Sum32()
+
+	binaryWriteFields(hash, binary.BigEndian, raw.PreludeCRC)
+
+	if raw.HeadersLen > 0 {
+		hash.Write(raw.Headers)
+	}
+
+	// Read payload bytes and update hash for it as well.
+	if len(m.Payload) > 0 {
+		raw.Payload = m.Payload
+		hash.Write(raw.Payload)
+	}
+
+	raw.CRC = hash.Sum32()
+
+	return raw, nil
+}
+
+type messagePrelude struct {
+	Length     uint32
+	HeadersLen uint32
+	PreludeCRC uint32
+}
+
+func (p messagePrelude) PayloadLen() uint32 {
+	return p.Length - p.HeadersLen - minMsgLen
+}
+
+func (p messagePrelude) ValidateLens() error {
+	if p.Length == 0 || p.Length > maxMsgLen {
+		return LengthError{
+			Part: "message prelude",
+			Want: maxMsgLen,
+			Have: int(p.Length),
+		}
+	}
+	if p.HeadersLen > maxHeadersLen {
+		return LengthError{
+			Part: "message headers",
+			Want: maxHeadersLen,
+			Have: int(p.HeadersLen),
+		}
+	}
+	if payloadLen := p.PayloadLen(); payloadLen > maxPayloadLen {
+		return LengthError{
+			Part: "message payload",
+			Want: maxPayloadLen,
+			Have: int(payloadLen),
+		}
+	}
+
+	return nil
+}
+
+type rawMessage struct {
+	messagePrelude
+
+	Headers []byte
+	Payload []byte
+
+	CRC uint32
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/payload.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/payload.go
new file mode 100644
index 00000000000..e21614a1250
--- /dev/null
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/payload.go
@@ -0,0 +1,81 @@
+package protocol
+
+import (
+	"io"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/client/metadata"
+	"github.com/aws/aws-sdk-go/aws/request"
+)
+
+// PayloadUnmarshaler provides the interface for unmarshaling a payload's
+// reader into a SDK shape.
+type PayloadUnmarshaler interface {
+	UnmarshalPayload(io.Reader, interface{}) error
+}
+
+// HandlerPayloadUnmarshal implements the PayloadUnmarshaler from a
+// HandlerList. This provides the support for unmarshaling a payload reader to
+// a shape without needing a SDK request first.
+type HandlerPayloadUnmarshal struct {
+	Unmarshalers request.HandlerList
+}
+
+// UnmarshalPayload unmarshals the io.Reader payload into the SDK shape using
+// the Unmarshalers HandlerList provided. Returns an error if unable
+// unmarshaling fails.
+func (h HandlerPayloadUnmarshal) UnmarshalPayload(r io.Reader, v interface{}) error {
+	req := &request.Request{
+		HTTPRequest: &http.Request{},
+		HTTPResponse: &http.Response{
+			StatusCode: 200,
+			Header:     http.Header{},
+			Body:       ioutil.NopCloser(r),
+		},
+		Data: v,
+	}
+
+	h.Unmarshalers.Run(req)
+
+	return req.Error
+}
+
+// PayloadMarshaler provides the interface for marshaling a SDK shape into and
+// io.Writer.
+type PayloadMarshaler interface {
+	MarshalPayload(io.Writer, interface{}) error
+}
+
+// HandlerPayloadMarshal implements the PayloadMarshaler from a HandlerList.
+// This provides support for marshaling a SDK shape into an io.Writer without
+// needing a SDK request first.
+type HandlerPayloadMarshal struct {
+	Marshalers request.HandlerList
+}
+
+// MarshalPayload marshals the SDK shape into the io.Writer using the
+// Marshalers HandlerList provided. Returns an error if unable if marshal
+// fails.
+func (h HandlerPayloadMarshal) MarshalPayload(w io.Writer, v interface{}) error {
+	req := request.New(
+		aws.Config{},
+		metadata.ClientInfo{},
+		request.Handlers{},
+		nil,
+		&request.Operation{HTTPMethod: "GET"},
+		v,
+		nil,
+	)
+
+	h.Marshalers.Run(req)
+
+	if req.Error != nil {
+		return req.Error
+	}
+
+	io.Copy(w, req.GetBody())
+
+	return nil
+}
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/rest/build.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/rest/build.go
index c405288d742..f761e0b3a5b 100644
--- a/vendor/github.com/aws/aws-sdk-go/private/protocol/rest/build.go
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/rest/build.go
@@ -20,8 +20,10 @@ import (
 	"github.com/aws/aws-sdk-go/private/protocol"
 )
 
-// RFC822 returns an RFC822 formatted timestamp for AWS protocols
-const RFC822 = "Mon, 2 Jan 2006 15:04:05 GMT"
+// RFC1123GMT is a RFC1123 (RFC822) formated timestame. This format is not
+// using the standard library's time.RFC1123 due to the desire to always use
+// GMT as the timezone.
+const RFC1123GMT = "Mon, 2 Jan 2006 15:04:05 GMT"
 
 // Whether the byte value can be sent without escaping in AWS URLs
 var noEscape [256]bool
@@ -270,7 +272,7 @@ func convertType(v reflect.Value, tag reflect.StructTag) (str string, err error)
 	case float64:
 		str = strconv.FormatFloat(value, 'f', -1, 64)
 	case time.Time:
-		str = value.UTC().Format(RFC822)
+		str = value.UTC().Format(RFC1123GMT)
 	case aws.JSONValue:
 		if len(value) == 0 {
 			return "", errValueNotSet
diff --git a/vendor/github.com/aws/aws-sdk-go/private/protocol/rest/unmarshal.go b/vendor/github.com/aws/aws-sdk-go/private/protocol/rest/unmarshal.go
index 823f045eed7..9d4e7626775 100644
--- a/vendor/github.com/aws/aws-sdk-go/private/protocol/rest/unmarshal.go
+++ b/vendor/github.com/aws/aws-sdk-go/private/protocol/rest/unmarshal.go
@@ -198,7 +198,7 @@ func unmarshalHeader(v reflect.Value, header string, tag reflect.StructTag) erro
 		}
 		v.Set(reflect.ValueOf(&f))
 	case *time.Time:
-		t, err := time.Parse(RFC822, header)
+		t, err := time.Parse(time.RFC1123, header)
 		if err != nil {
 			return err
 		}
diff --git a/vendor/github.com/aws/aws-sdk-go/service/cloudwatch/service.go b/vendor/github.com/aws/aws-sdk-go/service/cloudwatch/service.go
index 4b0aa76edcd..0d478662240 100644
--- a/vendor/github.com/aws/aws-sdk-go/service/cloudwatch/service.go
+++ b/vendor/github.com/aws/aws-sdk-go/service/cloudwatch/service.go
@@ -29,8 +29,9 @@ var initRequest func(*request.Request)
 
 // Service information constants
 const (
-	ServiceName = "monitoring" // Service endpoint prefix API calls made to.
-	EndpointsID = ServiceName  // Service ID for Regions and Endpoints metadata.
+	ServiceName = "monitoring" // Name of service.
+	EndpointsID = ServiceName  // ID to lookup a service endpoint with.
+	ServiceID   = "CloudWatch" // ServiceID is a unique identifer of a specific service.
 )
 
 // New creates a new instance of the CloudWatch client with a session.
@@ -55,6 +56,7 @@ func newClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
 			cfg,
 			metadata.ClientInfo{
 				ServiceName:   ServiceName,
+				ServiceID:     ServiceID,
 				SigningName:   signingName,
 				SigningRegion: signingRegion,
 				Endpoint:      endpoint,
diff --git a/vendor/github.com/aws/aws-sdk-go/service/ec2/api.go b/vendor/github.com/aws/aws-sdk-go/service/ec2/api.go
index 99d12a66e42..b48e40e205c 100644
--- a/vendor/github.com/aws/aws-sdk-go/service/ec2/api.go
+++ b/vendor/github.com/aws/aws-sdk-go/service/ec2/api.go
@@ -2268,11 +2268,7 @@ func (c *EC2) CancelSpotInstanceRequestsRequest(input *CancelSpotInstanceRequest
 
 // CancelSpotInstanceRequests API operation for Amazon Elastic Compute Cloud.
 //
-// Cancels one or more Spot Instance requests. Spot Instances are instances
-// that Amazon EC2 starts on your behalf when the maximum price that you specify
-// exceeds the current Spot price. For more information, see Spot Instance Requests
-// (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html) in
-// the Amazon EC2 User Guide for Linux Instances.
+// Cancels one or more Spot Instance requests.
 //
 // Canceling a Spot Instance request does not terminate running Spot Instances
 // associated with the request.
@@ -4179,8 +4175,8 @@ func (c *EC2) CreateNetworkInterfacePermissionRequest(input *CreateNetworkInterf
 
 // CreateNetworkInterfacePermission API operation for Amazon Elastic Compute Cloud.
 //
-// Grants an AWS authorized partner account permission to attach the specified
-// network interface to an instance in their account.
+// Grants an AWS-authorized account permission to attach the specified network
+// interface to an instance in their account.
 //
 // You can grant permission to a single AWS account only, and only one account
 // at a time.
@@ -13675,11 +13671,7 @@ func (c *EC2) DescribeSpotInstanceRequestsRequest(input *DescribeSpotInstanceReq
 
 // DescribeSpotInstanceRequests API operation for Amazon Elastic Compute Cloud.
 //
-// Describes the Spot Instance requests that belong to your account. Spot Instances
-// are instances that Amazon EC2 launches when the Spot price that you specify
-// exceeds the current Spot price. For more information, see Spot Instance Requests
-// (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html) in
-// the Amazon EC2 User Guide for Linux Instances.
+// Describes the specified Spot Instance requests.
 //
 // You can use DescribeSpotInstanceRequests to find a running Spot Instance
 // by examining the response. If the status of the Spot Instance is fulfilled,
@@ -21367,9 +21359,9 @@ func (c *EC2) RequestSpotInstancesRequest(input *RequestSpotInstancesInput) (req
 
 // RequestSpotInstances API operation for Amazon Elastic Compute Cloud.
 //
-// Creates a Spot Instance request. Spot Instances are instances that Amazon
-// EC2 launches when the maximum price that you specify exceeds the current
-// Spot price. For more information, see Spot Instance Requests (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html)
+// Creates a Spot Instance request.
+//
+// For more information, see Spot Instance Requests (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html)
 // in the Amazon EC2 User Guide for Linux Instances.
 //
 // Returns awserr.Error for service API and SDK errors. Use runtime type assertions
@@ -37615,7 +37607,7 @@ type DescribeInstancesInput struct {
 	// The maximum number of results to return in a single call. To retrieve the
 	// remaining results, make another call with the returned NextToken value. This
 	// value can be between 5 and 1000. You cannot specify this parameter and the
-	// instance IDs parameter or tag filters in the same call.
+	// instance IDs parameter in the same call.
 	MaxResults *int64 `locationName:"maxResults" type:"integer"`
 
 	// The token to request the next page of results.
@@ -66458,19 +66450,23 @@ type StateReason struct {
 
 	// The message for the state change.
 	//
-	//    * Server.InsufficientInstanceCapacity: There was insufficient instance
-	//    capacity to satisfy the launch request.
+	//    * Server.InsufficientInstanceCapacity: There was insufficient capacity
+	//    available to satisfy the launch request.
 	//
-	//    * Server.InternalError: An internal error occurred during instance launch,
-	//    resulting in termination.
+	//    * Server.InternalError: An internal error caused the instance to terminate
+	//    during launch.
 	//
 	//    * Server.ScheduledStop: The instance was stopped due to a scheduled retirement.
 	//
-	//    * Server.SpotInstanceTermination: A Spot Instance was terminated due to
-	//    an increase in the Spot price.
+	//    * Server.SpotInstanceShutdown: The instance was stopped because the number
+	//    of Spot requests with a maximum price equal to or higher than the Spot
+	//    price exceeded available capacity or because of an increase in the Spot
+	//    price.
 	//
-	//    * Client.InternalError: A client error caused the instance to terminate
-	//    on launch.
+	//    * Server.SpotInstanceTermination: The instance was terminated because
+	//    the number of Spot requests with a maximum price equal to or higher than
+	//    the Spot price exceeded available capacity or because of an increase in
+	//    the Spot price.
 	//
 	//    * Client.InstanceInitiatedShutdown: The instance was shut down using the
 	//    shutdown -h command from the instance.
@@ -66478,14 +66474,17 @@ type StateReason struct {
 	//    * Client.InstanceTerminated: The instance was terminated or rebooted during
 	//    AMI creation.
 	//
+	//    * Client.InternalError: A client error caused the instance to terminate
+	//    during launch.
+	//
+	//    * Client.InvalidSnapshot.NotFound: The specified snapshot was not found.
+	//
 	//    * Client.UserInitiatedShutdown: The instance was shut down using the Amazon
 	//    EC2 API.
 	//
 	//    * Client.VolumeLimitExceeded: The limit on the number of EBS volumes or
 	//    total storage was exceeded. Decrease usage or request an increase in your
-	//    limits.
-	//
-	//    * Client.InvalidSnapshot.NotFound: The specified snapshot was not found.
+	//    account limits.
 	Message *string `locationName:"message" type:"string"`
 }
 
@@ -66969,7 +66968,7 @@ type TagSpecification struct {
 	_ struct{} `type:"structure"`
 
 	// The type of resource to tag. Currently, the resource types that support tagging
-	// on creation are instance and volume.
+	// on creation are instance, snapshot, and volume.
 	ResourceType *string `locationName:"resourceType" type:"string" enum:"ResourceType"`
 
 	// The tags to apply to the resource.
@@ -70694,6 +70693,9 @@ const (
 	// InstanceTypeI316xlarge is a InstanceType enum value
 	InstanceTypeI316xlarge = "i3.16xlarge"
 
+	// InstanceTypeI3Metal is a InstanceType enum value
+	InstanceTypeI3Metal = "i3.metal"
+
 	// InstanceTypeHi14xlarge is a InstanceType enum value
 	InstanceTypeHi14xlarge = "hi1.4xlarge"
 
@@ -70754,6 +70756,24 @@ const (
 	// InstanceTypeC518xlarge is a InstanceType enum value
 	InstanceTypeC518xlarge = "c5.18xlarge"
 
+	// InstanceTypeC5dLarge is a InstanceType enum value
+	InstanceTypeC5dLarge = "c5d.large"
+
+	// InstanceTypeC5dXlarge is a InstanceType enum value
+	InstanceTypeC5dXlarge = "c5d.xlarge"
+
+	// InstanceTypeC5d2xlarge is a InstanceType enum value
+	InstanceTypeC5d2xlarge = "c5d.2xlarge"
+
+	// InstanceTypeC5d4xlarge is a InstanceType enum value
+	InstanceTypeC5d4xlarge = "c5d.4xlarge"
+
+	// InstanceTypeC5d9xlarge is a InstanceType enum value
+	InstanceTypeC5d9xlarge = "c5d.9xlarge"
+
+	// InstanceTypeC5d18xlarge is a InstanceType enum value
+	InstanceTypeC5d18xlarge = "c5d.18xlarge"
+
 	// InstanceTypeCc14xlarge is a InstanceType enum value
 	InstanceTypeCc14xlarge = "cc1.4xlarge"
 
@@ -70832,6 +70852,24 @@ const (
 	// InstanceTypeM524xlarge is a InstanceType enum value
 	InstanceTypeM524xlarge = "m5.24xlarge"
 
+	// InstanceTypeM5dLarge is a InstanceType enum value
+	InstanceTypeM5dLarge = "m5d.large"
+
+	// InstanceTypeM5dXlarge is a InstanceType enum value
+	InstanceTypeM5dXlarge = "m5d.xlarge"
+
+	// InstanceTypeM5d2xlarge is a InstanceType enum value
+	InstanceTypeM5d2xlarge = "m5d.2xlarge"
+
+	// InstanceTypeM5d4xlarge is a InstanceType enum value
+	InstanceTypeM5d4xlarge = "m5d.4xlarge"
+
+	// InstanceTypeM5d12xlarge is a InstanceType enum value
+	InstanceTypeM5d12xlarge = "m5d.12xlarge"
+
+	// InstanceTypeM5d24xlarge is a InstanceType enum value
+	InstanceTypeM5d24xlarge = "m5d.24xlarge"
+
 	// InstanceTypeH12xlarge is a InstanceType enum value
 	InstanceTypeH12xlarge = "h1.2xlarge"
 
diff --git a/vendor/github.com/aws/aws-sdk-go/service/ec2/service.go b/vendor/github.com/aws/aws-sdk-go/service/ec2/service.go
index ba4433d388e..6acbc43fe3d 100644
--- a/vendor/github.com/aws/aws-sdk-go/service/ec2/service.go
+++ b/vendor/github.com/aws/aws-sdk-go/service/ec2/service.go
@@ -29,8 +29,9 @@ var initRequest func(*request.Request)
 
 // Service information constants
 const (
-	ServiceName = "ec2"       // Service endpoint prefix API calls made to.
-	EndpointsID = ServiceName // Service ID for Regions and Endpoints metadata.
+	ServiceName = "ec2"       // Name of service.
+	EndpointsID = ServiceName // ID to lookup a service endpoint with.
+	ServiceID   = "EC2"       // ServiceID is a unique identifer of a specific service.
 )
 
 // New creates a new instance of the EC2 client with a session.
@@ -55,6 +56,7 @@ func newClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
 			cfg,
 			metadata.ClientInfo{
 				ServiceName:   ServiceName,
+				ServiceID:     ServiceID,
 				SigningName:   signingName,
 				SigningRegion: signingRegion,
 				Endpoint:      endpoint,
diff --git a/vendor/github.com/aws/aws-sdk-go/service/s3/api.go b/vendor/github.com/aws/aws-sdk-go/service/s3/api.go
index a27823fdfb5..07fc06af1f9 100644
--- a/vendor/github.com/aws/aws-sdk-go/service/s3/api.go
+++ b/vendor/github.com/aws/aws-sdk-go/service/s3/api.go
@@ -3,14 +3,21 @@
 package s3
 
 import (
+	"bytes"
 	"fmt"
 	"io"
+	"sync"
+	"sync/atomic"
 	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awsutil"
+	"github.com/aws/aws-sdk-go/aws/client"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/private/protocol"
+	"github.com/aws/aws-sdk-go/private/protocol/eventstream"
+	"github.com/aws/aws-sdk-go/private/protocol/eventstream/eventstreamapi"
+	"github.com/aws/aws-sdk-go/private/protocol/rest"
 	"github.com/aws/aws-sdk-go/private/protocol/restxml"
 )
 
@@ -6017,6 +6024,88 @@ func (c *S3) RestoreObjectWithContext(ctx aws.Context, input *RestoreObjectInput
 	return out, req.Send()
 }
 
+const opSelectObjectContent = "SelectObjectContent"
+
+// SelectObjectContentRequest generates a "aws/request.Request" representing the
+// client's request for the SelectObjectContent operation. The "output" return
+// value will be populated with the request's response once the request completes
+// successfuly.
+//
+// Use "Send" method on the returned Request to send the API call to the service.
+// the "output" return value is not valid until after Send returns without error.
+//
+// See SelectObjectContent for more information on using the SelectObjectContent
+// API call, and error handling.
+//
+// This method is useful when you want to inject custom logic or configuration
+// into the SDK's request lifecycle. Such as custom headers, or retry logic.
+//
+//
+//    // Example sending a request using the SelectObjectContentRequest method.
+//    req, resp := client.SelectObjectContentRequest(params)
+//
+//    err := req.Send()
+//    if err == nil { // resp is now filled
+//        fmt.Println(resp)
+//    }
+//
+// See also, https://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/SelectObjectContent
+func (c *S3) SelectObjectContentRequest(input *SelectObjectContentInput) (req *request.Request, output *SelectObjectContentOutput) {
+	op := &request.Operation{
+		Name:       opSelectObjectContent,
+		HTTPMethod: "POST",
+		HTTPPath:   "/{Bucket}/{Key+}?select&select-type=2",
+	}
+
+	if input == nil {
+		input = &SelectObjectContentInput{}
+	}
+
+	output = &SelectObjectContentOutput{}
+	req = c.newRequest(op, input, output)
+	req.Handlers.Send.Swap(client.LogHTTPResponseHandler.Name, client.LogHTTPResponseHeaderHandler)
+	req.Handlers.Unmarshal.Swap(restxml.UnmarshalHandler.Name, rest.UnmarshalHandler)
+	req.Handlers.Unmarshal.PushBack(output.runEventStreamLoop)
+	return
+}
+
+// SelectObjectContent API operation for Amazon Simple Storage Service.
+//
+// This operation filters the contents of an Amazon S3 object based on a simple
+// Structured Query Language (SQL) statement. In the request, along with the
+// SQL expression, you must also specify a data serialization format (JSON or
+// CSV) of the object. Amazon S3 uses this to parse object data into records,
+// and returns only records that match the specified SQL expression. You must
+// also specify the data serialization format for the response.
+//
+// Returns awserr.Error for service API and SDK errors. Use runtime type assertions
+// with awserr.Error's Code and Message methods to get detailed information about
+// the error.
+//
+// See the AWS API reference guide for Amazon Simple Storage Service's
+// API operation SelectObjectContent for usage and error information.
+// See also, https://docs.aws.amazon.com/goto/WebAPI/s3-2006-03-01/SelectObjectContent
+func (c *S3) SelectObjectContent(input *SelectObjectContentInput) (*SelectObjectContentOutput, error) {
+	req, out := c.SelectObjectContentRequest(input)
+	return out, req.Send()
+}
+
+// SelectObjectContentWithContext is the same as SelectObjectContent with the addition of
+// the ability to pass a context and additional request options.
+//
+// See SelectObjectContent for details on how to use this API operation.
+//
+// The context must be non-nil and will be used for request cancellation. If
+// the context is nil a panic will occur. In the future the SDK may create
+// sub-contexts for http.Requests. See https://golang.org/pkg/context/
+// for more information on using Contexts.
+func (c *S3) SelectObjectContentWithContext(ctx aws.Context, input *SelectObjectContentInput, opts ...request.Option) (*SelectObjectContentOutput, error) {
+	req, out := c.SelectObjectContentRequest(input)
+	req.SetContext(ctx)
+	req.ApplyOptions(opts...)
+	return out, req.Send()
+}
+
 const opUploadPart = "UploadPart"
 
 // UploadPartRequest generates a "aws/request.Request" representing the
@@ -7474,6 +7563,32 @@ func (s *Condition) SetKeyPrefixEquals(v string) *Condition {
 	return s
 }
 
+type ContinuationEvent struct {
+	_ struct{} `type:"structure"`
+}
+
+// String returns the string representation
+func (s ContinuationEvent) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s ContinuationEvent) GoString() string {
+	return s.String()
+}
+
+// The ContinuationEvent is and event in the SelectObjectContentEventStream group of events.
+func (s *ContinuationEvent) eventSelectObjectContentEventStream() {}
+
+// UnmarshalEvent unmarshals the EventStream Message into the ContinuationEvent value.
+// This method is only used internally within the SDK's EventStream handling.
+func (s *ContinuationEvent) UnmarshalEvent(
+	payloadUnmarshaler protocol.PayloadUnmarshaler,
+	msg eventstream.Message,
+) error {
+	return nil
+}
+
 type CopyObjectInput struct {
 	_ struct{} `type:"structure"`
 
@@ -9919,6 +10034,32 @@ func (s *EncryptionConfiguration) SetReplicaKmsKeyID(v string) *EncryptionConfig
 	return s
 }
 
+type EndEvent struct {
+	_ struct{} `type:"structure"`
+}
+
+// String returns the string representation
+func (s EndEvent) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s EndEvent) GoString() string {
+	return s.String()
+}
+
+// The EndEvent is and event in the SelectObjectContentEventStream group of events.
+func (s *EndEvent) eventSelectObjectContentEventStream() {}
+
+// UnmarshalEvent unmarshals the EventStream Message into the EndEvent value.
+// This method is only used internally within the SDK's EventStream handling.
+func (s *EndEvent) UnmarshalEvent(
+	payloadUnmarshaler protocol.PayloadUnmarshaler,
+	msg eventstream.Message,
+) error {
+	return nil
+}
+
 type Error struct {
 	_ struct{} `type:"structure"`
 
@@ -16380,6 +16521,87 @@ func (s *Part) SetSize(v int64) *Part {
 	return s
 }
 
+type Progress struct {
+	_ struct{} `type:"structure"`
+
+	// Current number of uncompressed object bytes processed.
+	BytesProcessed *int64 `type:"long"`
+
+	// Current number of bytes of records payload data returned.
+	BytesReturned *int64 `type:"long"`
+
+	// Current number of object bytes scanned.
+	BytesScanned *int64 `type:"long"`
+}
+
+// String returns the string representation
+func (s Progress) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s Progress) GoString() string {
+	return s.String()
+}
+
+// SetBytesProcessed sets the BytesProcessed field's value.
+func (s *Progress) SetBytesProcessed(v int64) *Progress {
+	s.BytesProcessed = &v
+	return s
+}
+
+// SetBytesReturned sets the BytesReturned field's value.
+func (s *Progress) SetBytesReturned(v int64) *Progress {
+	s.BytesReturned = &v
+	return s
+}
+
+// SetBytesScanned sets the BytesScanned field's value.
+func (s *Progress) SetBytesScanned(v int64) *Progress {
+	s.BytesScanned = &v
+	return s
+}
+
+type ProgressEvent struct {
+	_ struct{} `type:"structure" payload:"Details"`
+
+	// The Progress event details.
+	Details *Progress `locationName:"Details" type:"structure"`
+}
+
+// String returns the string representation
+func (s ProgressEvent) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s ProgressEvent) GoString() string {
+	return s.String()
+}
+
+// SetDetails sets the Details field's value.
+func (s *ProgressEvent) SetDetails(v *Progress) *ProgressEvent {
+	s.Details = v
+	return s
+}
+
+// The ProgressEvent is and event in the SelectObjectContentEventStream group of events.
+func (s *ProgressEvent) eventSelectObjectContentEventStream() {}
+
+// UnmarshalEvent unmarshals the EventStream Message into the ProgressEvent value.
+// This method is only used internally within the SDK's EventStream handling.
+func (s *ProgressEvent) UnmarshalEvent(
+	payloadUnmarshaler protocol.PayloadUnmarshaler,
+	msg eventstream.Message,
+) error {
+	if err := payloadUnmarshaler.UnmarshalPayload(
+		bytes.NewReader(msg.Payload), s,
+	); err != nil {
+		return fmt.Errorf("failed to unmarshal payload, %v", err)
+	}
+	return nil
+}
+
 type PutBucketAccelerateConfigurationInput struct {
 	_ struct{} `type:"structure" payload:"AccelerateConfiguration"`
 
@@ -18622,6 +18844,45 @@ func (s *QueueConfigurationDeprecated) SetQueue(v string) *QueueConfigurationDep
 	return s
 }
 
+type RecordsEvent struct {
+	_ struct{} `type:"structure" payload:"Payload"`
+
+	// The byte array of partial, one or more result records.
+	//
+	// Payload is automatically base64 encoded/decoded by the SDK.
+	Payload []byte `type:"blob"`
+}
+
+// String returns the string representation
+func (s RecordsEvent) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s RecordsEvent) GoString() string {
+	return s.String()
+}
+
+// SetPayload sets the Payload field's value.
+func (s *RecordsEvent) SetPayload(v []byte) *RecordsEvent {
+	s.Payload = v
+	return s
+}
+
+// The RecordsEvent is and event in the SelectObjectContentEventStream group of events.
+func (s *RecordsEvent) eventSelectObjectContentEventStream() {}
+
+// UnmarshalEvent unmarshals the EventStream Message into the RecordsEvent value.
+// This method is only used internally within the SDK's EventStream handling.
+func (s *RecordsEvent) UnmarshalEvent(
+	payloadUnmarshaler protocol.PayloadUnmarshaler,
+	msg eventstream.Message,
+) error {
+	s.Payload = make([]byte, len(msg.Payload))
+	copy(s.Payload, msg.Payload)
+	return nil
+}
+
 type Redirect struct {
 	_ struct{} `type:"structure"`
 
@@ -18939,6 +19200,30 @@ func (s *RequestPaymentConfiguration) SetPayer(v string) *RequestPaymentConfigur
 	return s
 }
 
+type RequestProgress struct {
+	_ struct{} `type:"structure"`
+
+	// Specifies whether periodic QueryProgress frames should be sent. Valid values:
+	// TRUE, FALSE. Default value: FALSE.
+	Enabled *bool `type:"boolean"`
+}
+
+// String returns the string representation
+func (s RequestProgress) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s RequestProgress) GoString() string {
+	return s.String()
+}
+
+// SetEnabled sets the Enabled field's value.
+func (s *RequestProgress) SetEnabled(v bool) *RequestProgress {
+	s.Enabled = &v
+	return s
+}
+
 type RestoreObjectInput struct {
 	_ struct{} `type:"structure" payload:"RestoreRequest"`
 
@@ -19392,6 +19677,436 @@ func (s SSES3) GoString() string {
 	return s.String()
 }
 
+// SelectObjectContentEventStream provides handling of EventStreams for
+// the SelectObjectContent API.
+//
+// Use this type to receive SelectObjectContentEventStream events. The events
+// can be read from the Events channel member.
+//
+// The events that can be received are:
+//
+//     * ContinuationEvent
+//     * EndEvent
+//     * ProgressEvent
+//     * RecordsEvent
+//     * StatsEvent
+type SelectObjectContentEventStream struct {
+	// Reader is the EventStream reader for the SelectObjectContentEventStream
+	// events. This value is automatically set by the SDK when the API call is made
+	// Use this member when unit testing your code with the SDK to mock out the
+	// EventStream Reader.
+	//
+	// Must not be nil.
+	Reader SelectObjectContentEventStreamReader
+
+	// StreamCloser is the io.Closer for the EventStream connection. For HTTP
+	// EventStream this is the response Body. The stream will be closed when
+	// the Close method of the EventStream is called.
+	StreamCloser io.Closer
+}
+
+// Close closes the EventStream. This will also cause the Events channel to be
+// closed. You can use the closing of the Events channel to terminate your
+// application's read from the API's EventStream.
+//
+// Will close the underlying EventStream reader. For EventStream over HTTP
+// connection this will also close the HTTP connection.
+//
+// Close must be called when done using the EventStream API. Not calling Close
+// may result in resource leaks.
+func (es *SelectObjectContentEventStream) Close() (err error) {
+	es.Reader.Close()
+	return es.Err()
+}
+
+// Err returns any error that occurred while reading EventStream Events from
+// the service API's response. Returns nil if there were no errors.
+func (es *SelectObjectContentEventStream) Err() error {
+	if err := es.Reader.Err(); err != nil {
+		return err
+	}
+	es.StreamCloser.Close()
+
+	return nil
+}
+
+// Events returns a channel to read EventStream Events from the
+// SelectObjectContent API.
+//
+// These events are:
+//
+//     * ContinuationEvent
+//     * EndEvent
+//     * ProgressEvent
+//     * RecordsEvent
+//     * StatsEvent
+func (es *SelectObjectContentEventStream) Events() <-chan SelectObjectContentEventStreamEvent {
+	return es.Reader.Events()
+}
+
+// SelectObjectContentEventStreamEvent groups together all EventStream
+// events read from the SelectObjectContent API.
+//
+// These events are:
+//
+//     * ContinuationEvent
+//     * EndEvent
+//     * ProgressEvent
+//     * RecordsEvent
+//     * StatsEvent
+type SelectObjectContentEventStreamEvent interface {
+	eventSelectObjectContentEventStream()
+}
+
+// SelectObjectContentEventStreamReader provides the interface for reading EventStream
+// Events from the SelectObjectContent API. The
+// default implementation for this interface will be SelectObjectContentEventStream.
+//
+// The reader's Close method must allow multiple concurrent calls.
+//
+// These events are:
+//
+//     * ContinuationEvent
+//     * EndEvent
+//     * ProgressEvent
+//     * RecordsEvent
+//     * StatsEvent
+type SelectObjectContentEventStreamReader interface {
+	// Returns a channel of events as they are read from the event stream.
+	Events() <-chan SelectObjectContentEventStreamEvent
+
+	// Close will close the underlying event stream reader. For event stream over
+	// HTTP this will also close the HTTP connection.
+	Close() error
+
+	// Returns any error that has occured while reading from the event stream.
+	Err() error
+}
+
+type readSelectObjectContentEventStream struct {
+	eventReader *eventstreamapi.EventReader
+	stream      chan SelectObjectContentEventStreamEvent
+	errVal      atomic.Value
+
+	done      chan struct{}
+	closeOnce sync.Once
+}
+
+func newReadSelectObjectContentEventStream(
+	reader io.ReadCloser,
+	unmarshalers request.HandlerList,
+	logger aws.Logger,
+	logLevel aws.LogLevelType,
+) *readSelectObjectContentEventStream {
+	r := &readSelectObjectContentEventStream{
+		stream: make(chan SelectObjectContentEventStreamEvent),
+		done:   make(chan struct{}),
+	}
+
+	r.eventReader = eventstreamapi.NewEventReader(
+		reader,
+		protocol.HandlerPayloadUnmarshal{
+			Unmarshalers: unmarshalers,
+		},
+		r.unmarshalerForEventType,
+	)
+	r.eventReader.UseLogger(logger, logLevel)
+
+	return r
+}
+
+// Close will close the underlying event stream reader. For EventStream over
+// HTTP this will also close the HTTP connection.
+func (r *readSelectObjectContentEventStream) Close() error {
+	r.closeOnce.Do(r.safeClose)
+
+	return r.Err()
+}
+
+func (r *readSelectObjectContentEventStream) safeClose() {
+	close(r.done)
+	err := r.eventReader.Close()
+	if err != nil {
+		r.errVal.Store(err)
+	}
+}
+
+func (r *readSelectObjectContentEventStream) Err() error {
+	if v := r.errVal.Load(); v != nil {
+		return v.(error)
+	}
+
+	return nil
+}
+
+func (r *readSelectObjectContentEventStream) Events() <-chan SelectObjectContentEventStreamEvent {
+	return r.stream
+}
+
+func (r *readSelectObjectContentEventStream) readEventStream() {
+	defer close(r.stream)
+
+	for {
+		event, err := r.eventReader.ReadEvent()
+		if err != nil {
+			if err == io.EOF {
+				return
+			}
+			select {
+			case <-r.done:
+				// If closed already ignore the error
+				return
+			default:
+			}
+			r.errVal.Store(err)
+			return
+		}
+
+		select {
+		case r.stream <- event.(SelectObjectContentEventStreamEvent):
+		case <-r.done:
+			return
+		}
+	}
+}
+
+func (r *readSelectObjectContentEventStream) unmarshalerForEventType(
+	eventType string,
+) (eventstreamapi.Unmarshaler, error) {
+	switch eventType {
+	case "Cont":
+		return &ContinuationEvent{}, nil
+
+	case "End":
+		return &EndEvent{}, nil
+
+	case "Progress":
+		return &ProgressEvent{}, nil
+
+	case "Records":
+		return &RecordsEvent{}, nil
+
+	case "Stats":
+		return &StatsEvent{}, nil
+	default:
+		return nil, fmt.Errorf(
+			"unknown event type name, %s, for SelectObjectContentEventStream", eventType)
+	}
+}
+
+// Request to filter the contents of an Amazon S3 object based on a simple Structured
+// Query Language (SQL) statement. In the request, along with the SQL expression,
+// you must also specify a data serialization format (JSON or CSV) of the object.
+// Amazon S3 uses this to parse object data into records, and returns only records
+// that match the specified SQL expression. You must also specify the data serialization
+// format for the response. For more information, go to S3Select API Documentation
+// (https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectSELECTContent.html)
+type SelectObjectContentInput struct {
+	_ struct{} `locationName:"SelectObjectContentRequest" type:"structure" xmlURI:"http://s3.amazonaws.com/doc/2006-03-01/"`
+
+	// The S3 Bucket.
+	//
+	// Bucket is a required field
+	Bucket *string `location:"uri" locationName:"Bucket" type:"string" required:"true"`
+
+	// The expression that is used to query the object.
+	//
+	// Expression is a required field
+	Expression *string `type:"string" required:"true"`
+
+	// The type of the provided expression (e.g., SQL).
+	//
+	// ExpressionType is a required field
+	ExpressionType *string `type:"string" required:"true" enum:"ExpressionType"`
+
+	// Describes the format of the data in the object that is being queried.
+	//
+	// InputSerialization is a required field
+	InputSerialization *InputSerialization `type:"structure" required:"true"`
+
+	// The Object Key.
+	//
+	// Key is a required field
+	Key *string `location:"uri" locationName:"Key" min:"1" type:"string" required:"true"`
+
+	// Describes the format of the data that you want Amazon S3 to return in response.
+	//
+	// OutputSerialization is a required field
+	OutputSerialization *OutputSerialization `type:"structure" required:"true"`
+
+	// Specifies if periodic request progress information should be enabled.
+	RequestProgress *RequestProgress `type:"structure"`
+
+	// The SSE Algorithm used to encrypt the object. For more information, go to
+	//  Server-Side Encryption (Using Customer-Provided Encryption Keys (https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html)
+	SSECustomerAlgorithm *string `location:"header" locationName:"x-amz-server-side-encryption-customer-algorithm" type:"string"`
+
+	// The SSE Customer Key. For more information, go to  Server-Side Encryption
+	// (Using Customer-Provided Encryption Keys (https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html)
+	SSECustomerKey *string `location:"header" locationName:"x-amz-server-side-encryption-customer-key" type:"string"`
+
+	// The SSE Customer Key MD5. For more information, go to  Server-Side Encryption
+	// (Using Customer-Provided Encryption Keys (https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html)
+	SSECustomerKeyMD5 *string `location:"header" locationName:"x-amz-server-side-encryption-customer-key-MD5" type:"string"`
+}
+
+// String returns the string representation
+func (s SelectObjectContentInput) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s SelectObjectContentInput) GoString() string {
+	return s.String()
+}
+
+// Validate inspects the fields of the type to determine if they are valid.
+func (s *SelectObjectContentInput) Validate() error {
+	invalidParams := request.ErrInvalidParams{Context: "SelectObjectContentInput"}
+	if s.Bucket == nil {
+		invalidParams.Add(request.NewErrParamRequired("Bucket"))
+	}
+	if s.Expression == nil {
+		invalidParams.Add(request.NewErrParamRequired("Expression"))
+	}
+	if s.ExpressionType == nil {
+		invalidParams.Add(request.NewErrParamRequired("ExpressionType"))
+	}
+	if s.InputSerialization == nil {
+		invalidParams.Add(request.NewErrParamRequired("InputSerialization"))
+	}
+	if s.Key == nil {
+		invalidParams.Add(request.NewErrParamRequired("Key"))
+	}
+	if s.Key != nil && len(*s.Key) < 1 {
+		invalidParams.Add(request.NewErrParamMinLen("Key", 1))
+	}
+	if s.OutputSerialization == nil {
+		invalidParams.Add(request.NewErrParamRequired("OutputSerialization"))
+	}
+
+	if invalidParams.Len() > 0 {
+		return invalidParams
+	}
+	return nil
+}
+
+// SetBucket sets the Bucket field's value.
+func (s *SelectObjectContentInput) SetBucket(v string) *SelectObjectContentInput {
+	s.Bucket = &v
+	return s
+}
+
+func (s *SelectObjectContentInput) getBucket() (v string) {
+	if s.Bucket == nil {
+		return v
+	}
+	return *s.Bucket
+}
+
+// SetExpression sets the Expression field's value.
+func (s *SelectObjectContentInput) SetExpression(v string) *SelectObjectContentInput {
+	s.Expression = &v
+	return s
+}
+
+// SetExpressionType sets the ExpressionType field's value.
+func (s *SelectObjectContentInput) SetExpressionType(v string) *SelectObjectContentInput {
+	s.ExpressionType = &v
+	return s
+}
+
+// SetInputSerialization sets the InputSerialization field's value.
+func (s *SelectObjectContentInput) SetInputSerialization(v *InputSerialization) *SelectObjectContentInput {
+	s.InputSerialization = v
+	return s
+}
+
+// SetKey sets the Key field's value.
+func (s *SelectObjectContentInput) SetKey(v string) *SelectObjectContentInput {
+	s.Key = &v
+	return s
+}
+
+// SetOutputSerialization sets the OutputSerialization field's value.
+func (s *SelectObjectContentInput) SetOutputSerialization(v *OutputSerialization) *SelectObjectContentInput {
+	s.OutputSerialization = v
+	return s
+}
+
+// SetRequestProgress sets the RequestProgress field's value.
+func (s *SelectObjectContentInput) SetRequestProgress(v *RequestProgress) *SelectObjectContentInput {
+	s.RequestProgress = v
+	return s
+}
+
+// SetSSECustomerAlgorithm sets the SSECustomerAlgorithm field's value.
+func (s *SelectObjectContentInput) SetSSECustomerAlgorithm(v string) *SelectObjectContentInput {
+	s.SSECustomerAlgorithm = &v
+	return s
+}
+
+// SetSSECustomerKey sets the SSECustomerKey field's value.
+func (s *SelectObjectContentInput) SetSSECustomerKey(v string) *SelectObjectContentInput {
+	s.SSECustomerKey = &v
+	return s
+}
+
+func (s *SelectObjectContentInput) getSSECustomerKey() (v string) {
+	if s.SSECustomerKey == nil {
+		return v
+	}
+	return *s.SSECustomerKey
+}
+
+// SetSSECustomerKeyMD5 sets the SSECustomerKeyMD5 field's value.
+func (s *SelectObjectContentInput) SetSSECustomerKeyMD5(v string) *SelectObjectContentInput {
+	s.SSECustomerKeyMD5 = &v
+	return s
+}
+
+type SelectObjectContentOutput struct {
+	_ struct{} `type:"structure" payload:"Payload"`
+
+	// Use EventStream to use the API's stream.
+	EventStream *SelectObjectContentEventStream `type:"structure"`
+}
+
+// String returns the string representation
+func (s SelectObjectContentOutput) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s SelectObjectContentOutput) GoString() string {
+	return s.String()
+}
+
+// SetEventStream sets the EventStream field's value.
+func (s *SelectObjectContentOutput) SetEventStream(v *SelectObjectContentEventStream) *SelectObjectContentOutput {
+	s.EventStream = v
+	return s
+}
+
+func (s *SelectObjectContentOutput) runEventStreamLoop(r *request.Request) {
+	if r.Error != nil {
+		return
+	}
+	reader := newReadSelectObjectContentEventStream(
+		r.HTTPResponse.Body,
+		r.Handlers.UnmarshalStream,
+		r.Config.Logger,
+		r.Config.LogLevel.Value(),
+	)
+	go reader.readEventStream()
+
+	eventStream := &SelectObjectContentEventStream{
+		StreamCloser: r.HTTPResponse.Body,
+		Reader:       reader,
+	}
+	s.EventStream = eventStream
+}
+
 // Describes the parameters for Select job types.
 type SelectParameters struct {
 	_ struct{} `type:"structure"`
@@ -19696,6 +20411,87 @@ func (s *SseKmsEncryptedObjects) SetStatus(v string) *SseKmsEncryptedObjects {
 	return s
 }
 
+type Stats struct {
+	_ struct{} `type:"structure"`
+
+	// Total number of uncompressed object bytes processed.
+	BytesProcessed *int64 `type:"long"`
+
+	// Total number of bytes of records payload data returned.
+	BytesReturned *int64 `type:"long"`
+
+	// Total number of object bytes scanned.
+	BytesScanned *int64 `type:"long"`
+}
+
+// String returns the string representation
+func (s Stats) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s Stats) GoString() string {
+	return s.String()
+}
+
+// SetBytesProcessed sets the BytesProcessed field's value.
+func (s *Stats) SetBytesProcessed(v int64) *Stats {
+	s.BytesProcessed = &v
+	return s
+}
+
+// SetBytesReturned sets the BytesReturned field's value.
+func (s *Stats) SetBytesReturned(v int64) *Stats {
+	s.BytesReturned = &v
+	return s
+}
+
+// SetBytesScanned sets the BytesScanned field's value.
+func (s *Stats) SetBytesScanned(v int64) *Stats {
+	s.BytesScanned = &v
+	return s
+}
+
+type StatsEvent struct {
+	_ struct{} `type:"structure" payload:"Details"`
+
+	// The Stats event details.
+	Details *Stats `locationName:"Details" type:"structure"`
+}
+
+// String returns the string representation
+func (s StatsEvent) String() string {
+	return awsutil.Prettify(s)
+}
+
+// GoString returns the string representation
+func (s StatsEvent) GoString() string {
+	return s.String()
+}
+
+// SetDetails sets the Details field's value.
+func (s *StatsEvent) SetDetails(v *Stats) *StatsEvent {
+	s.Details = v
+	return s
+}
+
+// The StatsEvent is and event in the SelectObjectContentEventStream group of events.
+func (s *StatsEvent) eventSelectObjectContentEventStream() {}
+
+// UnmarshalEvent unmarshals the EventStream Message into the StatsEvent value.
+// This method is only used internally within the SDK's EventStream handling.
+func (s *StatsEvent) UnmarshalEvent(
+	payloadUnmarshaler protocol.PayloadUnmarshaler,
+	msg eventstream.Message,
+) error {
+	if err := payloadUnmarshaler.UnmarshalPayload(
+		bytes.NewReader(msg.Payload), s,
+	); err != nil {
+		return fmt.Errorf("failed to unmarshal payload, %v", err)
+	}
+	return nil
+}
+
 type StorageClassAnalysis struct {
 	_ struct{} `type:"structure"`
 
diff --git a/vendor/github.com/aws/aws-sdk-go/service/s3/service.go b/vendor/github.com/aws/aws-sdk-go/service/s3/service.go
index 614e477d3bb..20de53f29d7 100644
--- a/vendor/github.com/aws/aws-sdk-go/service/s3/service.go
+++ b/vendor/github.com/aws/aws-sdk-go/service/s3/service.go
@@ -29,8 +29,9 @@ var initRequest func(*request.Request)
 
 // Service information constants
 const (
-	ServiceName = "s3"        // Service endpoint prefix API calls made to.
-	EndpointsID = ServiceName // Service ID for Regions and Endpoints metadata.
+	ServiceName = "s3"        // Name of service.
+	EndpointsID = ServiceName // ID to lookup a service endpoint with.
+	ServiceID   = "S3"        // ServiceID is a unique identifer of a specific service.
 )
 
 // New creates a new instance of the S3 client with a session.
@@ -55,6 +56,7 @@ func newClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
 			cfg,
 			metadata.ClientInfo{
 				ServiceName:   ServiceName,
+				ServiceID:     ServiceID,
 				SigningName:   signingName,
 				SigningRegion: signingRegion,
 				Endpoint:      endpoint,
@@ -71,6 +73,8 @@ func newClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
 	svc.Handlers.UnmarshalMeta.PushBackNamed(restxml.UnmarshalMetaHandler)
 	svc.Handlers.UnmarshalError.PushBackNamed(restxml.UnmarshalErrorHandler)
 
+	svc.Handlers.UnmarshalStream.PushBackNamed(restxml.UnmarshalHandler)
+
 	// Run custom client initialization if present
 	if initClient != nil {
 		initClient(svc.Client)
diff --git a/vendor/github.com/aws/aws-sdk-go/service/sts/service.go b/vendor/github.com/aws/aws-sdk-go/service/sts/service.go
index 1ee5839e046..185c914d1b3 100644
--- a/vendor/github.com/aws/aws-sdk-go/service/sts/service.go
+++ b/vendor/github.com/aws/aws-sdk-go/service/sts/service.go
@@ -29,8 +29,9 @@ var initRequest func(*request.Request)
 
 // Service information constants
 const (
-	ServiceName = "sts"       // Service endpoint prefix API calls made to.
-	EndpointsID = ServiceName // Service ID for Regions and Endpoints metadata.
+	ServiceName = "sts"       // Name of service.
+	EndpointsID = ServiceName // ID to lookup a service endpoint with.
+	ServiceID   = "STS"       // ServiceID is a unique identifer of a specific service.
 )
 
 // New creates a new instance of the STS client with a session.
@@ -55,6 +56,7 @@ func newClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
 			cfg,
 			metadata.ClientInfo{
 				ServiceName:   ServiceName,
+				ServiceID:     ServiceID,
 				SigningName:   signingName,
 				SigningRegion: signingRegion,
 				Endpoint:      endpoint,
diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE
new file mode 100644
index 00000000000..c35c17af980
--- /dev/null
+++ b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2015 Dmitri Shuralyov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/main.go b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go
new file mode 100644
index 00000000000..6a77d124317
--- /dev/null
+++ b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go
@@ -0,0 +1,29 @@
+// Package sanitized_anchor_name provides a func to create sanitized anchor names.
+//
+// Its logic can be reused by multiple packages to create interoperable anchor names
+// and links to those anchors.
+//
+// At this time, it does not try to ensure that generated anchor names
+// are unique, that responsibility falls on the caller.
+package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name"
+
+import "unicode"
+
+// Create returns a sanitized anchor name for the given text.
+func Create(text string) string {
+	var anchorName []rune
+	var futureDash = false
+	for _, r := range text {
+		switch {
+		case unicode.IsLetter(r) || unicode.IsNumber(r):
+			if futureDash && len(anchorName) > 0 {
+				anchorName = append(anchorName, '-')
+			}
+			futureDash = false
+			anchorName = append(anchorName, unicode.ToLower(r))
+		default:
+			futureDash = true
+		}
+	}
+	return string(anchorName)
+}

From 40ed235b3ba29de41081ba59110824caa0671801 Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Mon, 16 Apr 2018 16:50:13 +0900
Subject: [PATCH 006/380] support GetMetricData

---
 pkg/metrics/metrics.go                        |   8 +
 pkg/tsdb/cloudwatch/cloudwatch.go             | 215 +++++++++++++++---
 pkg/tsdb/cloudwatch/types.go                  |   4 +
 .../datasource/cloudwatch/datasource.ts       |  20 +-
 .../cloudwatch/partials/query.parameter.html  |  45 ++--
 .../cloudwatch/query_parameter_ctrl.ts        |   3 +
 6 files changed, 246 insertions(+), 49 deletions(-)

diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 4dd84c12151..a8d9f7308fa 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -44,6 +44,7 @@ var (
 	M_Alerting_Notification_Sent         *prometheus.CounterVec
 	M_Aws_CloudWatch_GetMetricStatistics prometheus.Counter
 	M_Aws_CloudWatch_ListMetrics         prometheus.Counter
+	M_Aws_CloudWatch_GetMetricData       prometheus.Counter
 	M_DB_DataSource_QueryById            prometheus.Counter
 
 	// Timers
@@ -218,6 +219,12 @@ func init() {
 		Namespace: exporterName,
 	})
 
+	M_Aws_CloudWatch_GetMetricData = prometheus.NewCounter(prometheus.CounterOpts{
+		Name:      "aws_cloudwatch_get_metric_data_total",
+		Help:      "counter for getting metric data time series from aws",
+		Namespace: exporterName,
+	})
+
 	M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{
 		Name:      "db_datasource_query_by_id_total",
 		Help:      "counter for getting datasource by id",
@@ -307,6 +314,7 @@ func initMetricVars() {
 		M_Alerting_Notification_Sent,
 		M_Aws_CloudWatch_GetMetricStatistics,
 		M_Aws_CloudWatch_ListMetrics,
+		M_Aws_CloudWatch_GetMetricData,
 		M_DB_DataSource_QueryById,
 		M_Alerting_Active_Alerts,
 		M_StatTotal_Dashboards,
diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index 8af97575ae9..54634bc0614 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -14,6 +14,7 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
+	"golang.org/x/sync/errgroup"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/request"
@@ -88,48 +89,63 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 		Results: make(map[string]*tsdb.QueryResult),
 	}
 
-	errCh := make(chan error, 1)
-	resCh := make(chan *tsdb.QueryResult, 1)
+	eg, ectx := errgroup.WithContext(ctx)
 
-	currentlyExecuting := 0
+	getMetricDataQueries := make(map[string]map[string]*CloudWatchQuery)
 	for i, model := range queryContext.Queries {
 		queryType := model.Model.Get("type").MustString()
 		if queryType != "timeSeriesQuery" && queryType != "" {
 			continue
 		}
-		currentlyExecuting++
-		go func(refId string, index int) {
-			queryRes, err := e.executeQuery(ctx, queryContext.Queries[index].Model, queryContext)
-			currentlyExecuting--
-			if err != nil {
-				errCh <- err
-			} else {
-				queryRes.RefId = refId
-				resCh <- queryRes
+
+		query, err := parseQuery(queryContext.Queries[i].Model)
+		if err != nil {
+			return nil, err
+		}
+		query.RefId = queryContext.Queries[i].RefId
+
+		if query.Id != "" {
+			if _, ok := getMetricDataQueries[query.Region]; !ok {
+				getMetricDataQueries[query.Region] = make(map[string]*CloudWatchQuery)
 			}
-		}(model.RefId, i)
+			getMetricDataQueries[query.Region][query.Id] = query
+			continue
+		}
+
+		eg.Go(func() error {
+			queryRes, err := e.executeQuery(ectx, query, queryContext)
+			if err != nil {
+				return err
+			}
+			result.Results[queryRes.RefId] = queryRes
+			return nil
+		})
 	}
 
-	for currentlyExecuting != 0 {
-		select {
-		case res := <-resCh:
-			result.Results[res.RefId] = res
-		case err := <-errCh:
-			return result, err
-		case <-ctx.Done():
-			return result, ctx.Err()
+	if len(getMetricDataQueries) > 0 {
+		for region, getMetricDataQuery := range getMetricDataQueries {
+			q := getMetricDataQuery
+			eg.Go(func() error {
+				queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
+				if err != nil {
+					return err
+				}
+				for _, queryRes := range queryResponses {
+					result.Results[queryRes.RefId] = queryRes
+				}
+				return nil
+			})
 		}
 	}
 
+	if err := eg.Wait(); err != nil {
+		return nil, err
+	}
+
 	return result, nil
 }
 
-func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
-	query, err := parseQuery(parameters)
-	if err != nil {
-		return nil, err
-	}
-
+func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
 	client, err := e.getClient(query.Region)
 	if err != nil {
 		return nil, err
@@ -201,6 +217,139 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simpl
 	return queryRes, nil
 }
 
+func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, region string, queries map[string]*CloudWatchQuery, queryContext *tsdb.TsdbQuery) ([]*tsdb.QueryResult, error) {
+	queryResponses := make([]*tsdb.QueryResult, 0)
+
+	// validate query
+	for _, query := range queries {
+		if !(len(query.Statistics) == 1 && len(query.ExtendedStatistics) == 0) &&
+			!(len(query.Statistics) == 0 && len(query.ExtendedStatistics) == 1) {
+			return queryResponses, errors.New("Statistics count should be 1")
+		}
+	}
+
+	client, err := e.getClient(region)
+	if err != nil {
+		return queryResponses, err
+	}
+
+	startTime, err := queryContext.TimeRange.ParseFrom()
+	if err != nil {
+		return queryResponses, err
+	}
+
+	endTime, err := queryContext.TimeRange.ParseTo()
+	if err != nil {
+		return queryResponses, err
+	}
+
+	params := &cloudwatch.GetMetricDataInput{
+		StartTime: aws.Time(startTime),
+		EndTime:   aws.Time(endTime),
+		ScanBy:    aws.String("TimestampAscending"),
+	}
+	for _, query := range queries {
+		// 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600
+		if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) {
+			return nil, errors.New("too long query period")
+		}
+
+		mdq := &cloudwatch.MetricDataQuery{
+			Id:         aws.String(query.Id),
+			ReturnData: aws.Bool(query.ReturnData),
+		}
+		if query.Expression != "" {
+			mdq.Expression = aws.String(query.Expression)
+		} else {
+			mdq.MetricStat = &cloudwatch.MetricStat{
+				Metric: &cloudwatch.Metric{
+					Namespace:  aws.String(query.Namespace),
+					MetricName: aws.String(query.MetricName),
+				},
+				Period: aws.Int64(int64(query.Period)),
+			}
+			for _, d := range query.Dimensions {
+				mdq.MetricStat.Metric.Dimensions = append(mdq.MetricStat.Metric.Dimensions,
+					&cloudwatch.Dimension{
+						Name:  d.Name,
+						Value: d.Value,
+					})
+			}
+			if len(query.Statistics) == 1 {
+				mdq.MetricStat.Stat = query.Statistics[0]
+			} else {
+				mdq.MetricStat.Stat = query.ExtendedStatistics[0]
+			}
+		}
+		params.MetricDataQueries = append(params.MetricDataQueries, mdq)
+	}
+
+	nextToken := ""
+	mdr := make(map[string]*cloudwatch.MetricDataResult)
+	for {
+		if nextToken != "" {
+			params.NextToken = aws.String(nextToken)
+		}
+		resp, err := client.GetMetricDataWithContext(ctx, params)
+		if err != nil {
+			return queryResponses, err
+		}
+		metrics.M_Aws_CloudWatch_GetMetricData.Add(float64(len(params.MetricDataQueries)))
+
+		for _, r := range resp.MetricDataResults {
+			if _, ok := mdr[*r.Id]; !ok {
+				mdr[*r.Id] = r
+			} else {
+				mdr[*r.Id].Timestamps = append(mdr[*r.Id].Timestamps, r.Timestamps...)
+				mdr[*r.Id].Values = append(mdr[*r.Id].Values, r.Values...)
+			}
+		}
+
+		if resp.NextToken == nil || *resp.NextToken == "" {
+			break
+		}
+		nextToken = *resp.NextToken
+	}
+
+	for i, r := range mdr {
+		if *r.StatusCode != "Complete" {
+			return queryResponses, fmt.Errorf("Part of query is failed: %s", *r.StatusCode)
+		}
+
+		queryRes := tsdb.NewQueryResult()
+		queryRes.RefId = queries[i].RefId
+		query := queries[*r.Id]
+
+		series := tsdb.TimeSeries{
+			Tags:   map[string]string{},
+			Points: make([]tsdb.TimePoint, 0),
+		}
+		for _, d := range query.Dimensions {
+			series.Tags[*d.Name] = *d.Value
+		}
+		s := ""
+		if len(query.Statistics) == 1 {
+			s = *query.Statistics[0]
+		} else {
+			s = *query.ExtendedStatistics[0]
+		}
+		series.Name = formatAlias(query, s, series.Tags)
+
+		for j, t := range r.Timestamps {
+			expectedTimestamp := r.Timestamps[j].Add(time.Duration(query.Period) * time.Second)
+			if j > 0 && expectedTimestamp.Before(*t) {
+				series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(expectedTimestamp.Unix()*1000)))
+			}
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(*r.Values[j]), float64((*t).Unix())*1000))
+		}
+
+		queryRes.Series = append(queryRes.Series, &series)
+		queryResponses = append(queryResponses, queryRes)
+	}
+
+	return queryResponses, nil
+}
+
 func parseDimensions(model *simplejson.Json) ([]*cloudwatch.Dimension, error) {
 	var result []*cloudwatch.Dimension
 
@@ -257,6 +406,9 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
 		return nil, err
 	}
 
+	id := model.Get("id").MustString("")
+	expression := model.Get("expression").MustString("")
+
 	dimensions, err := parseDimensions(model)
 	if err != nil {
 		return nil, err
@@ -295,6 +447,7 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
 		alias = "{{metric}}_{{stat}}"
 	}
 
+	returnData := model.Get("returnData").MustBool(false)
 	highResolution := model.Get("highResolution").MustBool(false)
 
 	return &CloudWatchQuery{
@@ -306,11 +459,18 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
 		ExtendedStatistics: aws.StringSlice(extendedStatistics),
 		Period:             period,
 		Alias:              alias,
+		Id:                 id,
+		Expression:         expression,
+		ReturnData:         returnData,
 		HighResolution:     highResolution,
 	}, nil
 }
 
 func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string {
+	if len(query.Id) > 0 && len(query.Expression) > 0 {
+		return query.Id
+	}
+
 	data := map[string]string{}
 	data["region"] = query.Region
 	data["namespace"] = query.Namespace
@@ -338,6 +498,7 @@ func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]stri
 func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatchQuery) (*tsdb.QueryResult, error) {
 	queryRes := tsdb.NewQueryResult()
 
+	queryRes.RefId = query.RefId
 	var value float64
 	for _, s := range append(query.Statistics, query.ExtendedStatistics...) {
 		series := tsdb.TimeSeries{
diff --git a/pkg/tsdb/cloudwatch/types.go b/pkg/tsdb/cloudwatch/types.go
index 0737b64686d..1225fb9b31b 100644
--- a/pkg/tsdb/cloudwatch/types.go
+++ b/pkg/tsdb/cloudwatch/types.go
@@ -5,6 +5,7 @@ import (
 )
 
 type CloudWatchQuery struct {
+	RefId              string
 	Region             string
 	Namespace          string
 	MetricName         string
@@ -13,5 +14,8 @@ type CloudWatchQuery struct {
 	ExtendedStatistics []*string
 	Period             int
 	Alias              string
+	Id                 string
+	Expression         string
+	ReturnData         bool
 	HighResolution     bool
 }
diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts
index 4101759ec1d..74100e5d69a 100644
--- a/public/app/plugins/datasource/cloudwatch/datasource.ts
+++ b/public/app/plugins/datasource/cloudwatch/datasource.ts
@@ -30,7 +30,9 @@ export default class CloudWatchDatasource {
 
     var queries = _.filter(options.targets, item => {
       return (
-        item.hide !== true && !!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)
+        (item.id !== '' || item.hide !== true) &&
+        ((!!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)) ||
+          item.expression.length > 0)
       );
     }).map(item => {
       item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars);
@@ -38,6 +40,9 @@ export default class CloudWatchDatasource {
       item.metricName = this.templateSrv.replace(item.metricName, options.scopedVars);
       item.dimensions = this.convertDimensionFormat(item.dimensions, options.scopedVars);
       item.period = String(this.getPeriod(item, options)); // use string format for period in graph query, and alerting
+      item.id = this.templateSrv.replace(item.id, options.scopedVars);
+      item.expression = this.templateSrv.replace(item.expression, options.scopedVars);
+      item.returnData = typeof item.hide === 'undefined' ? true : !item.hide;
 
       return _.extend(
         {
@@ -384,11 +389,11 @@ export default class CloudWatchDatasource {
     var currentVariables = !_.isArray(variable.current.value)
       ? [variable.current]
       : variable.current.value.map(v => {
-          return {
-            text: v,
-            value: v,
-          };
-        });
+        return {
+          text: v,
+          value: v,
+        };
+      });
     let useSelectedVariables =
       selectedVariables.some(s => {
         return s.value === currentVariables[0].value;
@@ -399,6 +404,9 @@ export default class CloudWatchDatasource {
       scopedVar[variable.name] = v;
       t.refId = target.refId + '_' + v.value;
       t.dimensions[dimensionKey] = templateSrv.replace(t.dimensions[dimensionKey], scopedVar);
+      if (target.id) {
+        t.id = target.id + window.btoa(v.value).replace(/=/g, '0'); // generate unique id
+      }
       return t;
     });
   }
diff --git a/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html b/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
index 81bad39e23a..57a59f80265 100644
--- a/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
+++ b/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
@@ -1,4 +1,4 @@
-<div class="gf-form-inline">
+<div class="gf-form-inline" ng-if="target.expression.length === 0">
 	<div class="gf-form">
 		<label class="gf-form-label query-keyword width-8">Metric</label>
 
@@ -20,7 +20,7 @@
 	</div>
 </div>
 
-<div class="gf-form-inline">
+<div class="gf-form-inline" ng-if="target.expression.length === 0">
 	<div class="gf-form">
 		<label class="gf-form-label query-keyword width-8">Dimensions</label>
 		<metric-segment ng-repeat="segment in dimSegments" segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
@@ -31,18 +31,31 @@
 	</div>
 </div>
 
-<div class="gf-form-inline">
+<div class="gf-form-inline" ng-if="target.statistics.length === 1">
 	<div class="gf-form">
-		<label class="gf-form-label query-keyword width-8">
-			Min period
-			<info-popover mode="right-normal">Minimum interval between points in seconds</info-popover>
-		</label>
-		<input type="text" class="gf-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="onChange()" />
+		<label class=" gf-form-label query-keyword width-8 ">Id</label>
+		<input type="text " class="gf-form-input " ng-model="target.id " spellcheck='false' ng-model-onblur ng-change="onChange() ">
 	</div>
-	<div class="gf-form max-width-30">
-		<label class="gf-form-label query-keyword width-7">Alias</label>
-		<input type="text" class="gf-form-input"  ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="onChange()">
-		<info-popover mode="right-absolute">
+	<div class="gf-form max-width-30 ">
+		<label class="gf-form-label query-keyword width-7 ">Expression</label>
+		<input type="text " class="gf-form-input " ng-model="target.expression
+	 " spellcheck='false' ng-model-onblur ng-change="onChange() ">
+	</div>
+</div>
+
+<div class="gf-form-inline ">
+	<div class="gf-form ">
+		<label class="gf-form-label query-keyword width-8 ">
+			Min period
+			<info-popover mode="right-normal ">Minimum interval between points in seconds</info-popover>
+		</label>
+		<input type="text " class="gf-form-input " ng-model="target.period " spellcheck='false' placeholder="auto
+	 " ng-model-onblur ng-change="onChange() " />
+	</div>
+	<div class="gf-form max-width-30 ">
+		<label class="gf-form-label query-keyword width-7 ">Alias</label>
+		<input type="text " class="gf-form-input " ng-model="target.alias " spellcheck='false' ng-model-onblur ng-change="onChange() ">
+		<info-popover mode="right-absolute ">
 			Alias replacement variables:
 			<ul ng-non-bindable>
 				<li>{{metric}}</li>
@@ -54,12 +67,12 @@
 			</ul>
 		</info-popover>
 	</div>
-	<div class="gf-form">
-		<gf-form-switch class="gf-form" label="HighRes" label-class="width-5" checked="target.highResolution" on-change="onChange()">
+	<div class="gf-form ">
+		<gf-form-switch class="gf-form " label="HighRes " label-class="width-5 " checked="target.highResolution " on-change="onChange() ">
 		</gf-form-switch>
 	</div>
 
-	<div class="gf-form gf-form--grow">
-		<div class="gf-form-label gf-form-label--grow"></div>
+	<div class="gf-form gf-form--grow ">
+		<div class="gf-form-label gf-form-label--grow "></div>
 	</div>
 </div>
diff --git a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts
index 0b47ebd7069..689cf270feb 100644
--- a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts
+++ b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts
@@ -27,6 +27,9 @@ export class CloudWatchQueryParameterCtrl {
       target.dimensions = target.dimensions || {};
       target.period = target.period || '';
       target.region = target.region || 'default';
+      target.id = target.id || '';
+      target.expression = target.expression || '';
+      target.returnData = target.returnData || false;
       target.highResolution = target.highResolution || false;
 
       $scope.regionSegment = uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');

From 4c59be4f5b0c4b3841a8190ec79139a581c3db84 Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Fri, 22 Jun 2018 16:25:04 +0900
Subject: [PATCH 007/380] generate unique id when variable is multi

---
 .../plugins/datasource/cloudwatch/datasource.ts    | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts
index 74100e5d69a..41e335dc320 100644
--- a/public/app/plugins/datasource/cloudwatch/datasource.ts
+++ b/public/app/plugins/datasource/cloudwatch/datasource.ts
@@ -389,11 +389,11 @@ export default class CloudWatchDatasource {
     var currentVariables = !_.isArray(variable.current.value)
       ? [variable.current]
       : variable.current.value.map(v => {
-        return {
-          text: v,
-          value: v,
-        };
-      });
+          return {
+            text: v,
+            value: v,
+          };
+        });
     let useSelectedVariables =
       selectedVariables.some(s => {
         return s.value === currentVariables[0].value;
@@ -404,8 +404,10 @@ export default class CloudWatchDatasource {
       scopedVar[variable.name] = v;
       t.refId = target.refId + '_' + v.value;
       t.dimensions[dimensionKey] = templateSrv.replace(t.dimensions[dimensionKey], scopedVar);
-      if (target.id) {
+      if (variable.multi && target.id) {
         t.id = target.id + window.btoa(v.value).replace(/=/g, '0'); // generate unique id
+      } else {
+        t.id = target.id;
       }
       return t;
     });

From 77220456b6c5478e3dc0addfc3838008fb1ea2a5 Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Fri, 22 Jun 2018 16:35:17 +0900
Subject: [PATCH 008/380] improve error message

---
 pkg/tsdb/cloudwatch/cloudwatch.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index 54634bc0614..38fbac3aa29 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -112,6 +112,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 			continue
 		}
 
+		if query.Id == "" && query.Expression != "" {
+			return nil, fmt.Errorf("Invalid query: id should be set if using expression")
+		}
+
 		eg.Go(func() error {
 			queryRes, err := e.executeQuery(ectx, query, queryContext)
 			if err != nil {

From 4ee4ca99be159862a8990034e0087417174dfd09 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 10 Jul 2018 12:54:45 +0200
Subject: [PATCH 009/380] Prevent scroll on focus for iframe

---
 public/app/core/components/scroll/page_scroll.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/app/core/components/scroll/page_scroll.ts b/public/app/core/components/scroll/page_scroll.ts
index e6db344a4d6..0cb36eba914 100644
--- a/public/app/core/components/scroll/page_scroll.ts
+++ b/public/app/core/components/scroll/page_scroll.ts
@@ -29,11 +29,11 @@ export function pageScrollbar() {
       scope.$on('$routeChangeSuccess', () => {
         lastPos = 0;
         elem[0].scrollTop = 0;
-        elem[0].focus();
+        elem[0].focus({ preventScroll: true });
       });
 
       elem[0].tabIndex = -1;
-      elem[0].focus();
+      elem[0].focus({ preventScroll: true });
     },
   };
 }

From daf0c374b363d81d2ff36f44317a64279b31aa3b Mon Sep 17 00:00:00 2001
From: "Bryan T. Richardson" <btr@darkcubed.com>
Date: Tue, 10 Jul 2018 10:11:39 -0600
Subject: [PATCH 010/380] Added BurstBalance metric to list of AWS RDS metrics.

---
 pkg/tsdb/cloudwatch/metric_find_query.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index 136ee241c2e..e8e2c894120 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -92,7 +92,7 @@ func init() {
 		"AWS/NetworkELB":       {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
 		"AWS/OpsWorks":         {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
 		"AWS/Redshift":         {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
-		"AWS/RDS":              {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+		"AWS/RDS":              {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
 		"AWS/Route53":          {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
 		"AWS/S3":               {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
 		"AWS/SES":              {"Bounce", "Complaint", "Delivery", "Reject", "Send"},

From 81e62e105143f9493169d86a20bc2dd0766dab38 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Thu, 12 Jul 2018 13:16:41 +0200
Subject: [PATCH 011/380] Fix freezing browser when loading plugin

- broken since 4d2dd2209
- `*` was previously working as a path matcher, but freezes browser when
  used with new cache-busting plugin loader
- changed matcher to be `/*`
---
 public/app/features/plugins/plugin_loader.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts
index 20023e27b5c..641b5100703 100644
--- a/public/app/features/plugins/plugin_loader.ts
+++ b/public/app/features/plugins/plugin_loader.ts
@@ -56,7 +56,7 @@ System.config({
     css: 'vendor/plugin-css/css.js',
   },
   meta: {
-    '*': {
+    '/*': {
       esModule: true,
       authorization: true,
       loader: 'plugin-loader',

From 7361d352bf0fa503cc9775d5ceba39a2b6775e9b Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 12 Jul 2018 15:38:41 +0200
Subject: [PATCH 012/380] Add comments

---
 public/app/core/components/scroll/page_scroll.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/public/app/core/components/scroll/page_scroll.ts b/public/app/core/components/scroll/page_scroll.ts
index 0cb36eba914..b6603f06175 100644
--- a/public/app/core/components/scroll/page_scroll.ts
+++ b/public/app/core/components/scroll/page_scroll.ts
@@ -29,10 +29,12 @@ export function pageScrollbar() {
       scope.$on('$routeChangeSuccess', () => {
         lastPos = 0;
         elem[0].scrollTop = 0;
+        // Focus page to enable scrolling by keyboard
         elem[0].focus({ preventScroll: true });
       });
 
       elem[0].tabIndex = -1;
+      // Focus page to enable scrolling by keyboard
       elem[0].focus({ preventScroll: true });
     },
   };

From 756c08e713ad2d1be7aad681aee6db7c85d8791f Mon Sep 17 00:00:00 2001
From: Shane <smiller393@gmail.com>
Date: Fri, 13 Jul 2018 02:56:37 -0400
Subject: [PATCH 013/380] changed you to your (#12590)

---
 docs/sources/reference/templating.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index 8341b9770bd..efe9db61e3d 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -11,7 +11,7 @@ weight = 1
 # Variables
 
 Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
-and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
+and sensor name in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
 the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 
 {{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}}

From d06b26de262c1dccb9976d506fdc8e6f39b16118 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 13 Jul 2018 09:09:36 +0200
Subject: [PATCH 014/380] Explore Datasource selector

Adds a datasource selector to the Explore UI. Only datasource plugins
that have `explore: true` in their `plugin.json` can be selected.

- adds datasource selector (based on react-select) to explore UI
- adds getExploreSources to datasource service
- new `explore` flag in datasource plugins model
- Prometheus plugin enabled explore
---
 pkg/plugins/datasource_plugin.go              |  1 +
 public/app/containers/Explore/Explore.tsx     | 92 +++++++++++++++----
 public/app/features/plugins/datasource_srv.ts | 33 ++++---
 .../plugins/specs/datasource_srv.jest.ts      | 30 +++++-
 .../plugins/datasource/prometheus/plugin.json | 30 ++++--
 public/sass/pages/_explore.scss               |  4 +
 6 files changed, 148 insertions(+), 42 deletions(-)

diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go
index 2fec6acbf54..cef35a2e7d9 100644
--- a/pkg/plugins/datasource_plugin.go
+++ b/pkg/plugins/datasource_plugin.go
@@ -22,6 +22,7 @@ type DataSourcePlugin struct {
 	Annotations  bool              `json:"annotations"`
 	Metrics      bool              `json:"metrics"`
 	Alerting     bool              `json:"alerting"`
+	Explore      bool              `json:"explore"`
 	QueryOptions map[string]bool   `json:"queryOptions,omitempty"`
 	BuiltIn      bool              `json:"builtIn,omitempty"`
 	Mixed        bool              `json:"mixed,omitempty"`
diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index deebe84f2c8..90bf0941572 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -1,16 +1,17 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
+import Select from 'react-select';
+
 import colors from 'app/core/utils/colors';
 import TimeSeries from 'app/core/time_series2';
+import { decodePathComponent } from 'app/core/utils/location_util';
 
 import ElapsedTime from './ElapsedTime';
 import QueryRows from './QueryRows';
 import Graph from './Graph';
 import Table from './Table';
 import TimePicker, { DEFAULT_RANGE } from './TimePicker';
-import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
-import { decodePathComponent } from 'app/core/utils/location_util';
 
 function makeTimeSeriesList(dataList, options) {
   return dataList.map((seriesData, index) => {
@@ -46,7 +47,8 @@ function parseInitialState(initial) {
 interface IExploreState {
   datasource: any;
   datasourceError: any;
-  datasourceLoading: any;
+  datasourceLoading: boolean | null;
+  datasourceMissing: boolean;
   graphResult: any;
   latency: number;
   loading: any;
@@ -61,15 +63,14 @@ interface IExploreState {
 
 // @observer
 export class Explore extends React.Component<any, IExploreState> {
-  datasourceSrv: DatasourceSrv;
-
   constructor(props) {
     super(props);
     const { range, queries } = parseInitialState(props.routeParams.initial);
     this.state = {
       datasource: null,
       datasourceError: null,
-      datasourceLoading: true,
+      datasourceLoading: null,
+      datasourceMissing: false,
       graphResult: null,
       latency: 0,
       loading: false,
@@ -85,19 +86,43 @@ export class Explore extends React.Component<any, IExploreState> {
   }
 
   async componentDidMount() {
-    const datasource = await this.props.datasourceSrv.get();
-    const testResult = await datasource.testDatasource();
-    if (testResult.status === 'success') {
-      this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
+    const { datasourceSrv } = this.props;
+    if (!datasourceSrv) {
+      throw new Error('No datasource service passed as props.');
+    }
+    const datasources = datasourceSrv.getExploreSources();
+    if (datasources.length > 0) {
+      this.setState({ datasourceLoading: true });
+      // Try default datasource, otherwise get first
+      let datasource = await datasourceSrv.get();
+      if (!datasource.meta.explore) {
+        datasource = await datasourceSrv.get(datasources[0].name);
+      }
+      this.setDatasource(datasource);
     } else {
-      this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
+      this.setState({ datasourceMissing: true });
     }
   }
 
   componentDidCatch(error) {
+    this.setState({ datasourceError: error });
     console.error(error);
   }
 
+  async setDatasource(datasource) {
+    try {
+      const testResult = await datasource.testDatasource();
+      if (testResult.status === 'success') {
+        this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
+      } else {
+        this.setState({ datasource: datasource, datasourceError: testResult.message, datasourceLoading: false });
+      }
+    } catch (error) {
+      const message = (error && error.statusText) || error;
+      this.setState({ datasource: datasource, datasourceError: message, datasourceLoading: false });
+    }
+  }
+
   handleAddQueryRow = index => {
     const { queries } = this.state;
     const nextQueries = [
@@ -108,6 +133,18 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState({ queries: nextQueries });
   };
 
+  handleChangeDatasource = async option => {
+    this.setState({
+      datasource: null,
+      datasourceError: null,
+      datasourceLoading: true,
+      graphResult: null,
+      tableResult: null,
+    });
+    const datasource = await this.props.datasourceSrv.get(option.value);
+    this.setDatasource(datasource);
+  };
+
   handleChangeQuery = (query, index) => {
     const { queries } = this.state;
     const nextQuery = {
@@ -226,11 +263,12 @@ export class Explore extends React.Component<any, IExploreState> {
   };
 
   render() {
-    const { position, split } = this.props;
+    const { datasourceSrv, position, split } = this.props;
     const {
       datasource,
       datasourceError,
       datasourceLoading,
+      datasourceMissing,
       graphResult,
       latency,
       loading,
@@ -247,6 +285,12 @@ export class Explore extends React.Component<any, IExploreState> {
     const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
     const tableButtonActive = showingBoth || showingTable ? 'active' : '';
     const exploreClass = split ? 'explore explore-split' : 'explore';
+    const datasources = datasourceSrv.getExploreSources().map(ds => ({
+      value: ds.name,
+      label: ds.name,
+    }));
+    const selectedDatasource = datasource ? datasource.name : undefined;
+
     return (
       <div className={exploreClass}>
         <div className="navbar">
@@ -264,6 +308,18 @@ export class Explore extends React.Component<any, IExploreState> {
               </button>
             </div>
           )}
+          {!datasourceMissing ? (
+            <div className="navbar-buttons">
+              <Select
+                className="datasource-picker"
+                clearable={false}
+                onChange={this.handleChangeDatasource}
+                options={datasources}
+                placeholder="Loading datasources..."
+                value={selectedDatasource}
+              />
+            </div>
+          ) : null}
           <div className="navbar__spacer" />
           {position === 'left' && !split ? (
             <div className="navbar-buttons">
@@ -291,13 +347,15 @@ export class Explore extends React.Component<any, IExploreState> {
 
         {datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
 
-        {datasourceError ? (
-          <div className="explore-container" title={datasourceError}>
-            Error connecting to datasource.
-          </div>
+        {datasourceMissing ? (
+          <div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
         ) : null}
 
-        {datasource ? (
+        {datasourceError ? (
+          <div className="explore-container">Error connecting to datasource. [{datasourceError}]</div>
+        ) : null}
+
+        {datasource && !datasourceError ? (
           <div className="explore-container">
             <QueryRows
               queries={queries}
diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts
index bff6f8b9f6a..8a29d30d582 100644
--- a/public/app/features/plugins/datasource_srv.ts
+++ b/public/app/features/plugins/datasource_srv.ts
@@ -7,7 +7,7 @@ export class DatasourceSrv {
   datasources: any;
 
   /** @ngInject */
-  constructor(private $q, private $injector, private $rootScope, private templateSrv) {
+  constructor(private $injector, private $rootScope, private templateSrv) {
     this.init();
   }
 
@@ -27,27 +27,25 @@ export class DatasourceSrv {
     }
 
     if (this.datasources[name]) {
-      return this.$q.when(this.datasources[name]);
+      return Promise.resolve(this.datasources[name]);
     }
 
     return this.loadDatasource(name);
   }
 
   loadDatasource(name) {
-    var dsConfig = config.datasources[name];
+    const dsConfig = config.datasources[name];
     if (!dsConfig) {
-      return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
+      return Promise.reject({ message: 'Datasource named ' + name + ' was not found' });
     }
 
-    var deferred = this.$q.defer();
-    var pluginDef = dsConfig.meta;
+    const pluginDef = dsConfig.meta;
 
-    importPluginModule(pluginDef.module)
+    return importPluginModule(pluginDef.module)
       .then(plugin => {
         // check if its in cache now
         if (this.datasources[name]) {
-          deferred.resolve(this.datasources[name]);
-          return;
+          return this.datasources[name];
         }
 
         // plugin module needs to export a constructor function named Datasource
@@ -55,17 +53,15 @@ export class DatasourceSrv {
           throw new Error('Plugin module is missing Datasource constructor');
         }
 
-        var instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
+        const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
         instance.meta = pluginDef;
         instance.name = name;
         this.datasources[name] = instance;
-        deferred.resolve(instance);
+        return instance;
       })
       .catch(err => {
         this.$rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]);
       });
-
-    return deferred.promise;
   }
 
   getAll() {
@@ -73,7 +69,7 @@ export class DatasourceSrv {
   }
 
   getAnnotationSources() {
-    var sources = [];
+    const sources = [];
 
     this.addDataSourceVariables(sources);
 
@@ -86,6 +82,14 @@ export class DatasourceSrv {
     return sources;
   }
 
+  getExploreSources() {
+    const { datasources } = config;
+    const es = Object.keys(datasources)
+      .map(name => datasources[name])
+      .filter(ds => ds.meta && ds.meta.explore);
+    return _.sortBy(es, ['name']);
+  }
+
   getMetricSources(options) {
     var metricSources = [];
 
@@ -155,3 +159,4 @@ export class DatasourceSrv {
 }
 
 coreModule.service('datasourceSrv', DatasourceSrv);
+export default DatasourceSrv;
diff --git a/public/app/features/plugins/specs/datasource_srv.jest.ts b/public/app/features/plugins/specs/datasource_srv.jest.ts
index 5458662ef9b..10381460d1c 100644
--- a/public/app/features/plugins/specs/datasource_srv.jest.ts
+++ b/public/app/features/plugins/specs/datasource_srv.jest.ts
@@ -16,10 +16,36 @@ const templateSrv = {
 };
 
 describe('datasource_srv', function() {
-  let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv);
-  let metricSources;
+  let _datasourceSrv = new DatasourceSrv({}, {}, templateSrv);
+
+  describe('when loading explore sources', () => {
+    beforeEach(() => {
+      config.datasources = {
+        explore1: {
+          name: 'explore1',
+          meta: { explore: true, metrics: true },
+        },
+        explore2: {
+          name: 'explore2',
+          meta: { explore: true, metrics: false },
+        },
+        nonExplore: {
+          name: 'nonExplore',
+          meta: { explore: false, metrics: true },
+        },
+      };
+    });
+
+    it('should return list of explore sources', () => {
+      const exploreSources = _datasourceSrv.getExploreSources();
+      expect(exploreSources.length).toBe(2);
+      expect(exploreSources[0].name).toBe('explore1');
+      expect(exploreSources[1].name).toBe('explore2');
+    });
+  });
 
   describe('when loading metric sources', () => {
+    let metricSources;
     let unsortedDatasources = {
       mmm: {
         type: 'test-db',
diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json
index 88847765159..2b723fd0b9d 100644
--- a/public/app/plugins/datasource/prometheus/plugin.json
+++ b/public/app/plugins/datasource/prometheus/plugin.json
@@ -2,21 +2,30 @@
   "type": "datasource",
   "name": "Prometheus",
   "id": "prometheus",
-
   "includes": [
-    {"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"},
-    {"type": "dashboard", "name": "Prometheus 2.0 Stats", "path": "dashboards/prometheus_2_stats.json"},
-    {"type": "dashboard", "name": "Grafana Stats", "path": "dashboards/grafana_stats.json"}
+    {
+      "type": "dashboard",
+      "name": "Prometheus Stats",
+      "path": "dashboards/prometheus_stats.json"
+    },
+    {
+      "type": "dashboard",
+      "name": "Prometheus 2.0 Stats",
+      "path": "dashboards/prometheus_2_stats.json"
+    },
+    {
+      "type": "dashboard",
+      "name": "Grafana Stats",
+      "path": "dashboards/grafana_stats.json"
+    }
   ],
-
   "metrics": true,
   "alerting": true,
   "annotations": true,
-
+  "explore": true,
   "queryOptions": {
     "minInterval": true
   },
-
   "info": {
     "description": "Prometheus Data Source for Grafana",
     "author": {
@@ -28,8 +37,11 @@
       "large": "img/prometheus_logo.svg"
     },
     "links": [
-      {"name": "Prometheus", "url": "https://prometheus.io/"}
+      {
+        "name": "Prometheus",
+        "url": "https://prometheus.io/"
+      }
     ],
     "version": "5.0.0"
   }
-}
+}
\ No newline at end of file
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index 876260c4f76..9ef549e022c 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -60,6 +60,10 @@
     flex-wrap: wrap;
   }
 
+  .datasource-picker {
+    min-width: 6rem;
+  }
+
   .timepicker {
     display: flex;
 

From 390090da0534b88bcc3972f6f62ced7a7994b13a Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 13 Jul 2018 09:45:56 +0200
Subject: [PATCH 015/380] Set datasource in deep links to Explore

---
 public/app/containers/Explore/Explore.tsx         | 15 ++++++++++++---
 .../plugins/datasource/prometheus/datasource.ts   |  1 +
 public/sass/pages/_explore.scss                   |  2 +-
 3 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 90bf0941572..81e1922d2cd 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -35,6 +35,7 @@ function parseInitialState(initial) {
   try {
     const parsed = JSON.parse(decodePathComponent(initial));
     return {
+      datasource: parsed.datasource,
       queries: parsed.queries.map(q => q.query),
       range: parsed.range,
     };
@@ -50,6 +51,7 @@ interface IExploreState {
   datasourceLoading: boolean | null;
   datasourceMissing: boolean;
   graphResult: any;
+  initialDatasource?: string;
   latency: number;
   loading: any;
   queries: any;
@@ -65,13 +67,14 @@ interface IExploreState {
 export class Explore extends React.Component<any, IExploreState> {
   constructor(props) {
     super(props);
-    const { range, queries } = parseInitialState(props.routeParams.initial);
+    const { datasource, queries, range } = parseInitialState(props.routeParams.initial);
     this.state = {
       datasource: null,
       datasourceError: null,
       datasourceLoading: null,
       datasourceMissing: false,
       graphResult: null,
+      initialDatasource: datasource,
       latency: 0,
       loading: false,
       queries: ensureQueries(queries),
@@ -87,14 +90,20 @@ export class Explore extends React.Component<any, IExploreState> {
 
   async componentDidMount() {
     const { datasourceSrv } = this.props;
+    const { initialDatasource } = this.state;
     if (!datasourceSrv) {
       throw new Error('No datasource service passed as props.');
     }
     const datasources = datasourceSrv.getExploreSources();
     if (datasources.length > 0) {
       this.setState({ datasourceLoading: true });
-      // Try default datasource, otherwise get first
-      let datasource = await datasourceSrv.get();
+      // Priority: datasource in url, default datasource, first explore datasource
+      let datasource;
+      if (initialDatasource) {
+        datasource = await datasourceSrv.get(initialDatasource);
+      } else {
+        datasource = await datasourceSrv.get();
+      }
       if (!datasource.meta.explore) {
         datasource = await datasourceSrv.get(datasources[0].name);
       }
diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 88d6141696d..9ccda65a145 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -357,6 +357,7 @@ export class PrometheusDatasource {
       state = {
         ...state,
         queries,
+        datasource: this.name,
       };
     }
     return state;
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index 9ef549e022c..e1b170c636d 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -61,7 +61,7 @@
   }
 
   .datasource-picker {
-    min-width: 6rem;
+    min-width: 10rem;
   }
 
   .timepicker {

From 964620c38c556fe801362adf6031bc9d0e99799e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Fri, 13 Jul 2018 05:07:04 -0700
Subject: [PATCH 016/380] fix: panel embedd scrolbar fix, fixes #12589 (#12595)

---
 public/sass/pages/_dashboard.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss
index 9b79279b99b..970b625c4f8 100644
--- a/public/sass/pages/_dashboard.scss
+++ b/public/sass/pages/_dashboard.scss
@@ -16,6 +16,7 @@ div.flot-text {
   height: 100%;
 
   &--solo {
+    margin: 0;
     .panel-container {
       border: none;
       z-index: $zindex-sidemenu + 1;

From 7ae844518cf2fa3ac90dc2a4b32ecd1c21674040 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <dehrax@users.noreply.github.com>
Date: Fri, 13 Jul 2018 21:10:09 +0200
Subject: [PATCH 017/380] Remove unused SASS variables (#12603)

* Comment out unused vars

* Remove unused sass vars
---
 public/sass/_variables.dark.scss  | 36 -------------------------
 public/sass/_variables.light.scss | 31 ---------------------
 public/sass/_variables.scss       | 45 +++++++------------------------
 3 files changed, 9 insertions(+), 103 deletions(-)

diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss
index 4907540815d..eb73b014a93 100644
--- a/public/sass/_variables.dark.scss
+++ b/public/sass/_variables.dark.scss
@@ -93,24 +93,14 @@ $headings-color: darken($white, 11%);
 $abbr-border-color: $gray-3 !default;
 $text-muted: $text-color-weak;
 
-$blockquote-small-color: $gray-3 !default;
-$blockquote-border-color: $gray-4 !default;
-
 $hr-border-color: rgba(0, 0, 0, 0.1) !default;
 
-// Components
-$component-active-color: #fff !default;
-$component-active-bg: $brand-primary !default;
-
 // Panel
 // -------------------------
 $panel-bg: #212124;
 $panel-border-color: $dark-1;
 $panel-border: solid 1px $panel-border-color;
-$panel-drop-zone-bg: repeating-linear-gradient(-128deg, #111, #111 10px, #191919 10px, #222 20px);
 $panel-header-hover-bg: $dark-4;
-$panel-header-menu-hover-bg: $dark-5;
-$panel-edit-shadow: 0 -30px 30px -30px $black;
 
 // page header
 $page-header-bg: linear-gradient(90deg, #292a2d, black);
@@ -205,7 +195,6 @@ $input-box-shadow-focus: rgba(102, 175, 233, 0.6);
 $input-color-placeholder: $gray-1 !default;
 $input-label-bg: $gray-blue;
 $input-label-border-color: $dark-3;
-$input-invalid-border-color: lighten($red, 5%);
 
 // Search
 $search-shadow: 0 0 30px 0 $black;
@@ -223,7 +212,6 @@ $dropdownBorder: rgba(0, 0, 0, 0.2);
 $dropdownDividerTop: transparent;
 $dropdownDividerBottom: #444;
 $dropdownDivider: $dropdownDividerBottom;
-$dropdownTitle: $link-color-disabled;
 
 $dropdownLinkColor: $text-color;
 $dropdownLinkColorHover: $white;
@@ -232,8 +220,6 @@ $dropdownLinkColorActive: $white;
 $dropdownLinkBackgroundActive: $dark-4;
 $dropdownLinkBackgroundHover: $dark-4;
 
-$dropdown-link-color: $gray-3;
-
 // COMPONENT VARIABLES
 // --------------------------------------------------
 
@@ -246,22 +232,13 @@ $horizontalComponentOffset: 180px;
 
 // Wells
 // -------------------------
-$wellBackground: #131517;
 
 $navbarHeight: 55px;
-$navbarBackgroundHighlight: $dark-3;
 $navbarBackground: $panel-bg;
 $navbarBorder: 1px solid $dark-3;
 $navbarShadow: 0 0 20px black;
 
-$navbarText: $gray-4;
 $navbarLinkColor: $gray-4;
-$navbarLinkColorHover: $white;
-$navbarLinkColorActive: $navbarLinkColorHover;
-$navbarLinkBackgroundHover: transparent;
-$navbarLinkBackgroundActive: $navbarBackground;
-$navbarBrandColor: $link-color;
-$navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg;
 
 $navbarButtonBackground: $navbarBackground;
 $navbarButtonBackgroundHighlight: $body-bg;
@@ -275,20 +252,15 @@ $side-menu-bg-mobile: $side-menu-bg;
 $side-menu-item-hover-bg: $dark-2;
 $side-menu-shadow: 0 0 20px black;
 $side-menu-link-color: $link-color;
-$breadcrumb-hover-hl: #111;
 
 // Menu dropdowns
 // -------------------------
 $menu-dropdown-bg: $body-bg;
 $menu-dropdown-hover-bg: $dark-2;
-$menu-dropdown-border-color: $dark-3;
 $menu-dropdown-shadow: 5px 5px 20px -5px $black;
 
 // Breadcrumb
 // -------------------------
-$page-nav-bg: $black;
-$page-nav-shadow: 5px 5px 20px -5px $black;
-$page-nav-breadcrumb-color: $gray-3;
 
 // Tabs
 // -------------------------
@@ -296,9 +268,6 @@ $tab-border-color: $dark-4;
 
 // Pagination
 // -------------------------
-$paginationBackground: $body-bg;
-$paginationBorder: transparent;
-$paginationActiveBackground: $blue;
 
 // Form states and alerts
 // -------------------------
@@ -343,10 +312,6 @@ $info-box-color: $gray-4;
 $footer-link-color: $gray-2;
 $footer-link-hover: $gray-4;
 
-// collapse box
-$collapse-box-body-border: $dark-5;
-$collapse-box-body-error-border: $red;
-
 // json-explorer
 $json-explorer-default-color: $text-color;
 $json-explorer-string-color: #23d662;
@@ -357,7 +322,6 @@ $json-explorer-undefined-color: rgb(239, 143, 190);
 $json-explorer-function-color: #fd48cb;
 $json-explorer-rotate-time: 100ms;
 $json-explorer-toggler-opacity: 0.6;
-$json-explorer-toggler-color: #45376f;
 $json-explorer-bracket-color: #9494ff;
 $json-explorer-key-color: #23a0db;
 $json-explorer-url-color: #027bff;
diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss
index 14716f6dfef..7e5e1b6a7f8 100644
--- a/public/sass/_variables.light.scss
+++ b/public/sass/_variables.light.scss
@@ -90,25 +90,15 @@ $headings-color: $text-color;
 $abbr-border-color: $gray-2 !default;
 $text-muted: $text-color-weak;
 
-$blockquote-small-color: $gray-2 !default;
-$blockquote-border-color: $gray-3 !default;
-
 $hr-border-color: $dark-3 !default;
 
-// Components
-$component-active-color: $white !default;
-$component-active-bg: $brand-primary !default;
-
 // Panel
 // -------------------------
 
 $panel-bg: $white;
 $panel-border-color: $gray-5;
 $panel-border: solid 1px $panel-border-color;
-$panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $gray-6 10px, $gray-6 20px);
 $panel-header-hover-bg: $gray-6;
-$panel-header-menu-hover-bg: $gray-4;
-$panel-edit-shadow: 0 0 30px 20px $black;
 
 // Page header
 $page-header-bg: linear-gradient(90deg, $white, $gray-7);
@@ -201,7 +191,6 @@ $input-box-shadow-focus: $blue !default;
 $input-color-placeholder: $gray-4 !default;
 $input-label-bg: $gray-5;
 $input-label-border-color: $gray-5;
-$input-invalid-border-color: lighten($red, 5%);
 
 // Sidemenu
 // -------------------------
@@ -215,15 +204,10 @@ $side-menu-link-color: $gray-6;
 // -------------------------
 $menu-dropdown-bg: $gray-7;
 $menu-dropdown-hover-bg: $gray-6;
-$menu-dropdown-border-color: $gray-4;
 $menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
 
 // Breadcrumb
 // -------------------------
-$page-nav-bg: $gray-5;
-$page-nav-shadow: 5px 5px 20px -5px $gray-4;
-$page-nav-breadcrumb-color: $black;
-$breadcrumb-hover-hl: #d9dadd;
 
 // Tabs
 // -------------------------
@@ -245,7 +229,6 @@ $dropdownBorder: $gray-4;
 $dropdownDividerTop: $gray-6;
 $dropdownDividerBottom: $white;
 $dropdownDivider: $dropdownDividerTop;
-$dropdownTitle: $gray-3;
 
 $dropdownLinkColor: $dark-3;
 $dropdownLinkColorHover: $link-color;
@@ -271,24 +254,16 @@ $horizontalComponentOffset: 180px;
 
 // Wells
 // -------------------------
-$wellBackground: $gray-3;
 
 // Navbar
 // -------------------------
 
 $navbarHeight: 52px;
-$navbarBackgroundHighlight: $white;
 $navbarBackground: $white;
 $navbarBorder: 1px solid $gray-4;
 $navbarShadow: 0 0 3px #c1c1c1;
 
-$navbarText: #444;
 $navbarLinkColor: #444;
-$navbarLinkColorHover: #000;
-$navbarLinkColorActive: #333;
-$navbarLinkBackgroundHover: transparent;
-$navbarLinkBackgroundActive: darken($navbarBackground, 6.5%);
-$navbarDropdownShadow: inset 0px 4px 7px -4px darken($body-bg, 20%);
 
 $navbarBrandColor: $navbarLinkColor;
 
@@ -299,9 +274,6 @@ $navbar-button-border: $gray-4;
 
 // Pagination
 // -------------------------
-$paginationBackground: $gray-2;
-$paginationBorder: transparent;
-$paginationActiveBackground: $blue;
 
 // Form states and alerts
 // -------------------------
@@ -346,8 +318,6 @@ $footer-link-color: $gray-3;
 $footer-link-hover: $dark-5;
 
 // collapse box
-$collapse-box-body-border: $gray-4;
-$collapse-box-body-error-border: $red;
 
 // json explorer
 $json-explorer-default-color: black;
@@ -359,7 +329,6 @@ $json-explorer-undefined-color: rgb(202, 11, 105);
 $json-explorer-function-color: #ff20ed;
 $json-explorer-rotate-time: 100ms;
 $json-explorer-toggler-opacity: 0.6;
-$json-explorer-toggler-color: #45376f;
 $json-explorer-bracket-color: blue;
 $json-explorer-key-color: #00008b;
 $json-explorer-url-color: blue;
diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss
index f46cacb0dd1..636b60c65a7 100644
--- a/public/sass/_variables.scss
+++ b/public/sass/_variables.scss
@@ -3,13 +3,7 @@
 // Quickly modify global styling by enabling or disabling optional features.
 
 $enable-flex: true !default;
-$enable-rounded: true !default;
-$enable-shadows: false !default;
-$enable-gradients: false !default;
-$enable-transitions: false !default;
 $enable-hover-media-query: false !default;
-$enable-grid-classes: true !default;
-$enable-print-styles: true !default;
 
 // Spacing
 //
@@ -53,9 +47,9 @@ $enable-flex: true;
 // Typography
 // -------------------------
 
-$font-family-sans-serif: "Roboto", Helvetica, Arial, sans-serif;
-$font-family-serif: Georgia, "Times New Roman", Times, serif;
-$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
+$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
+$font-family-serif: Georgia, 'Times New Roman', Times, serif;
+$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
 $font-family-base: $font-family-sans-serif !default;
 
 $font-size-root: 14px !default;
@@ -90,16 +84,12 @@ $lead-font-size: 1.25rem !default;
 $lead-font-weight: 300 !default;
 
 $headings-margin-bottom: ($spacer / 2) !default;
-$headings-font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-weight: 400 !default;
 $headings-line-height: 1.1 !default;
 
-$blockquote-font-size: ($font-size-base * 1.25) !default;
-$blockquote-border-width: 0.25rem !default;
-
 $hr-border-width: $border-width !default;
 $dt-font-weight: bold !default;
-$list-inline-padding: 5px !default;
 
 // Components
 //
@@ -112,9 +102,6 @@ $border-radius: 3px !default;
 $border-radius-lg: 5px !default;
 $border-radius-sm: 2px!default;
 
-$caret-width: 0.3em !default;
-$caret-width-lg: $caret-width !default;
-
 // Page
 
 $page-sidebar-width: 11rem;
@@ -130,7 +117,6 @@ $link-hover-decoration: none !default;
 // Customizes the `.table` component with basic values, each used across all table variations.
 
 $table-cell-padding: 4px 10px !default;
-$table-sm-cell-padding: 0.3rem !default;
 
 // Forms
 $input-padding-x: 10px !default;
@@ -139,31 +125,18 @@ $input-line-height: 18px !default;
 
 $input-btn-border-width: 1px;
 $input-border-radius: 0 $border-radius $border-radius 0 !default;
-$input-border-radius-lg: 0 $border-radius-lg $border-radius-lg 0 !default;
 $input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
 
 $label-border-radius: $border-radius 0 0 $border-radius !default;
-$label-border-radius-lg: $border-radius-lg 0 0 $border-radius-lg !default;
 $label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
 
-$input-padding-x-sm: 7px !default;
 $input-padding-y-sm: 4px !default;
 
 $input-padding-x-lg: 20px !default;
 $input-padding-y-lg: 10px !default;
 
-$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2))
-  !default;
-$input-height-lg: (
-     ($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2)
-  )
-  !default;
-$input-height-sm: (
-     ($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2)
-  )
-  !default;
+$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default;
 
-$form-group-margin-bottom: $spacer-y !default;
 $gf-form-margin: 0.2rem;
 
 $cursor-disabled: not-allowed !default;
@@ -221,9 +194,9 @@ $panel-padding: 0px 10px 5px 10px;
 $tabs-padding: 10px 15px 9px;
 
 $external-services: (
-    github: (bgColor: #464646, borderColor: #393939, icon: ""),
-    google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ""),
-    grafanacom: (bgColor: inherit, borderColor: #393939, icon: ""),
-    oauth: (bgColor: inherit, borderColor: #393939, icon: "")
+    github: (bgColor: #464646, borderColor: #393939, icon: ''),
+    google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''),
+    grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''),
+    oauth: (bgColor: inherit, borderColor: #393939, icon: '')
   )
   !default;

From 0f6e5e29531cfaa9c8c1e1ddf5fb6caaee24d42d Mon Sep 17 00:00:00 2001
From: Mark Meyer <mark@ofosos.org>
Date: Fri, 13 Jul 2018 21:14:40 +0200
Subject: [PATCH 018/380] Allow settting of default org id to auto-assign to
 (#12401)

Author:    Mark Meyer <mark@ofosos.org>
---
 docs/sources/installation/configuration.md |  6 ++++++
 pkg/services/sqlstore/dashboard_test.go    |  1 +
 pkg/services/sqlstore/org_test.go          |  1 +
 pkg/services/sqlstore/user.go              | 15 +++++++++++----
 pkg/setting/setting.go                     |  2 ++
 5 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 668a44fcb2b..f4fd6e49117 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -296,6 +296,12 @@ Set to `true` to automatically add new users to the main organization
 (id 1). When set to `false`, new users will automatically cause a new
 organization to be created for that new user.
 
+### auto_assign_org_id
+
+Set this value to automatically add new users to the provided org.
+This requires `auto_assign_org` to be set to `true`. Please make sure
+that this organization does already exists.
+
 ### auto_assign_org_role
 
 The role new users will be assigned for the main organization (if the
diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go
index e4aecf0391d..0ca1c5d67e4 100644
--- a/pkg/services/sqlstore/dashboard_test.go
+++ b/pkg/services/sqlstore/dashboard_test.go
@@ -387,6 +387,7 @@ func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isF
 
 func createUser(name string, role string, isAdmin bool) m.User {
 	setting.AutoAssignOrg = true
+	setting.AutoAssignOrgId = 1
 	setting.AutoAssignOrgRole = role
 
 	currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go
index 521a2a11c05..af8500707d5 100644
--- a/pkg/services/sqlstore/org_test.go
+++ b/pkg/services/sqlstore/org_test.go
@@ -17,6 +17,7 @@ func TestAccountDataAccess(t *testing.T) {
 
 		Convey("Given single org mode", func() {
 			setting.AutoAssignOrg = true
+			setting.AutoAssignOrgId = 1
 			setting.AutoAssignOrgRole = "Viewer"
 
 			Convey("Users should be added to default organization", func() {
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index 5e9a085b26d..0ec1a947870 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -42,16 +42,23 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error
 	var org m.Org
 
 	if setting.AutoAssignOrg {
-		// right now auto assign to org with id 1
-		has, err := sess.Where("id=?", 1).Get(&org)
+		has, err := sess.Where("id=?", setting.AutoAssignOrgId).Get(&org)
 		if err != nil {
 			return 0, err
 		}
 		if has {
 			return org.Id, nil
+		} else {
+			if setting.AutoAssignOrgId == 1 {
+				org.Name = "Main Org."
+				org.Id = int64(setting.AutoAssignOrgId)
+			} else {
+				sqlog.Info("Could not create user: organization id %v does not exist",
+					setting.AutoAssignOrgId)
+				return 0, fmt.Errorf("Could not create user: organization id %v does not exist",
+					setting.AutoAssignOrgId)
+			}
 		}
-		org.Name = "Main Org."
-		org.Id = 1
 	} else {
 		org.Name = cmd.OrgName
 		if len(org.Name) == 0 {
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index d8c8e6431c0..eb61568261d 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -100,6 +100,7 @@ var (
 	AllowUserSignUp         bool
 	AllowUserOrgCreate      bool
 	AutoAssignOrg           bool
+	AutoAssignOrgId         int
 	AutoAssignOrgRole       string
 	VerifyEmailEnabled      bool
 	LoginHint               string
@@ -592,6 +593,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
 	AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
 	AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
+	AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1)
 	AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
 	LoginHint = users.Key("login_hint").String()

From eb2abe800b150f238fc5b4d60861b9e37b59b6ad Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 13 Jul 2018 22:28:14 +0200
Subject: [PATCH 019/380] Reverted $q to Promise migration in datasource_srv

---
 public/app/features/plugins/datasource_srv.ts    | 16 ++++++++++------
 .../plugins/specs/datasource_srv.jest.ts         |  2 +-
 2 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts
index 8a29d30d582..df743640062 100644
--- a/public/app/features/plugins/datasource_srv.ts
+++ b/public/app/features/plugins/datasource_srv.ts
@@ -7,7 +7,7 @@ export class DatasourceSrv {
   datasources: any;
 
   /** @ngInject */
-  constructor(private $injector, private $rootScope, private templateSrv) {
+  constructor(private $q, private $injector, private $rootScope, private templateSrv) {
     this.init();
   }
 
@@ -27,7 +27,7 @@ export class DatasourceSrv {
     }
 
     if (this.datasources[name]) {
-      return Promise.resolve(this.datasources[name]);
+      return this.$q.when(this.datasources[name]);
     }
 
     return this.loadDatasource(name);
@@ -36,16 +36,18 @@ export class DatasourceSrv {
   loadDatasource(name) {
     const dsConfig = config.datasources[name];
     if (!dsConfig) {
-      return Promise.reject({ message: 'Datasource named ' + name + ' was not found' });
+      return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
     }
 
+    const deferred = this.$q.defer();
     const pluginDef = dsConfig.meta;
 
-    return importPluginModule(pluginDef.module)
+    importPluginModule(pluginDef.module)
       .then(plugin => {
         // check if its in cache now
         if (this.datasources[name]) {
-          return this.datasources[name];
+          deferred.resolve(this.datasources[name]);
+          return;
         }
 
         // plugin module needs to export a constructor function named Datasource
@@ -57,11 +59,13 @@ export class DatasourceSrv {
         instance.meta = pluginDef;
         instance.name = name;
         this.datasources[name] = instance;
-        return instance;
+        deferred.resolve(instance);
       })
       .catch(err => {
         this.$rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]);
       });
+
+    return deferred.promise;
   }
 
   getAll() {
diff --git a/public/app/features/plugins/specs/datasource_srv.jest.ts b/public/app/features/plugins/specs/datasource_srv.jest.ts
index 10381460d1c..b63e8537837 100644
--- a/public/app/features/plugins/specs/datasource_srv.jest.ts
+++ b/public/app/features/plugins/specs/datasource_srv.jest.ts
@@ -16,7 +16,7 @@ const templateSrv = {
 };
 
 describe('datasource_srv', function() {
-  let _datasourceSrv = new DatasourceSrv({}, {}, templateSrv);
+  let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv);
 
   describe('when loading explore sources', () => {
     beforeEach(() => {

From 2dd60f78d95d71c8c26843fce663f4b0b03f1aba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 16 Jul 2018 10:53:41 +0200
Subject: [PATCH 020/380] devenv: working on dev env setup & dashboards

---
 .../dev-dashboards/dashboard_with_rows.json   | 592 ------------------
 devenv/setup.sh                               |  20 +-
 2 files changed, 13 insertions(+), 599 deletions(-)
 delete mode 100644 devenv/dev-dashboards/dashboard_with_rows.json

diff --git a/devenv/dev-dashboards/dashboard_with_rows.json b/devenv/dev-dashboards/dashboard_with_rows.json
deleted file mode 100644
index 335c27bc80a..00000000000
--- a/devenv/dev-dashboards/dashboard_with_rows.json
+++ /dev/null
@@ -1,592 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "editable": true,
-  "gnetId": null,
-  "graphTooltip": 0,
-  "id": 59,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 9,
-      "panels": [],
-      "title": "Row title",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 1
-      },
-      "id": 12,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 5,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 5
-      },
-      "id": 7,
-      "panels": [],
-      "title": "Row",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 6
-      },
-      "id": 2,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 6
-      },
-      "id": 13,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 10
-      },
-      "id": 11,
-      "panels": [],
-      "title": "Row title",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 11
-      },
-      "id": 4,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 11
-      },
-      "id": 3,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "schemaVersion": 16,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": []
-  },
-  "time": {
-    "from": "now-30m",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "",
-  "title": "Dashboard with rows",
-  "uid": "1DdOzBNmk",
-  "version": 5
-}
diff --git a/devenv/setup.sh b/devenv/setup.sh
index 78dbfc1a366..6412bbc98ea 100755
--- a/devenv/setup.sh
+++ b/devenv/setup.sh
@@ -1,4 +1,4 @@
-#/bin/bash
+#!/bin/bash
 
 bulkDashboard() {
 
@@ -22,31 +22,37 @@ requiresJsonnet() {
 		fi
 }
 
-defaultDashboards() {
+devDashboards() {
+		echo -e "\xE2\x9C\x94 Setting up all dev dashboards using provisioning"
 		ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
 }
 
-defaultDatasources() {
-		echo "setting up all default datasources using provisioning"
+devDatasources() {
+		echo -e "\xE2\x9C\x94 Setting up all dev datasources using provisioning"
 
 		ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
 }
 
 usage() {
-	echo -e "install.sh\n\tThis script setups dev provision for datasources and dashboards"
+	echo -e "\n"
 	echo "Usage:"
 	echo "  bulk-dashboards                     - create and provisioning 400 dashboards"
 	echo "  no args                             - provisiong core datasources and dev dashboards"
 }
 
 main() {
+	echo -e "------------------------------------------------------------------"
+	echo -e "This script setups provisioning for dev datasources and dashboards"
+	echo -e "------------------------------------------------------------------"
+	echo -e "\n"
+
 	local cmd=$1
 
 	if [[ $cmd == "bulk-dashboards" ]]; then
 		bulkDashboard
 	else
-		defaultDashboards
-		defaultDatasources
+		devDashboards
+		devDatasources
 	fi
 
   if [[ -z "$cmd" ]]; then

From c6bcf13d780d13b2cf13ec280adddd25baf295a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 16 Jul 2018 03:12:13 -0700
Subject: [PATCH 021/380] Devenv testdata dashboards (#12615)

* devenv: working on dev env setup & dashboards

* devenv: refactored testdata app to a built in datasource instead, and moved dashboards to a devenv provisioned dashboards
---
 devenv/datasources.yaml                       |    3 +
 .../dev-dashboards/dashboard_with_rows.json   |  592 -------
 devenv/dev-dashboards/panel_tests_graph.json  | 1558 +++++++++++++++++
 .../panel_tests_singlestat.json               |  574 ++++++
 devenv/dev-dashboards/panel_tests_table.json  |  453 +++++
 .../dev-dashboards/testdata_alerts.json       |    6 +-
 devenv/setup.sh                               |   20 +-
 pkg/tsdb/testdata/testdata.go                 |    2 +-
 .../app/features/plugins/built_in_plugins.ts  |    7 +-
 .../testdata/dashboards/graph_last_1h.json    | 1448 ---------------
 public/app/plugins/app/testdata/module.ts     |   34 -
 public/app/plugins/app/testdata/plugin.json   |   32 -
 .../testdata}/datasource.ts                   |    0
 .../testdata}/module.ts                       |    0
 .../testdata/partials/query.editor.html       |    1 -
 .../testdata}/plugin.json                     |    8 +-
 .../testdata}/query_ctrl.ts                   |    0
 .../app/plugins/panel/singlestat/editor.html  |    4 +-
 18 files changed, 2613 insertions(+), 2129 deletions(-)
 delete mode 100644 devenv/dev-dashboards/dashboard_with_rows.json
 create mode 100644 devenv/dev-dashboards/panel_tests_graph.json
 create mode 100644 devenv/dev-dashboards/panel_tests_singlestat.json
 create mode 100644 devenv/dev-dashboards/panel_tests_table.json
 rename public/app/plugins/app/testdata/dashboards/alerts.json => devenv/dev-dashboards/testdata_alerts.json (98%)
 delete mode 100644 public/app/plugins/app/testdata/dashboards/graph_last_1h.json
 delete mode 100644 public/app/plugins/app/testdata/module.ts
 delete mode 100644 public/app/plugins/app/testdata/plugin.json
 rename public/app/plugins/{app/testdata/datasource => datasource/testdata}/datasource.ts (100%)
 rename public/app/plugins/{app/testdata/datasource => datasource/testdata}/module.ts (100%)
 rename public/app/plugins/{app => datasource}/testdata/partials/query.editor.html (99%)
 rename public/app/plugins/{app/testdata/datasource => datasource/testdata}/plugin.json (60%)
 rename public/app/plugins/{app/testdata/datasource => datasource/testdata}/query_ctrl.ts (100%)

diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml
index e93c0217f27..58368afdd27 100644
--- a/devenv/datasources.yaml
+++ b/devenv/datasources.yaml
@@ -14,6 +14,9 @@ datasources:
     isDefault: true
     url: http://localhost:9090
 
+  - name: gdev-testdata
+    type: testdata
+
   - name: gdev-influxdb
     type: influxdb
     access: proxy
diff --git a/devenv/dev-dashboards/dashboard_with_rows.json b/devenv/dev-dashboards/dashboard_with_rows.json
deleted file mode 100644
index 335c27bc80a..00000000000
--- a/devenv/dev-dashboards/dashboard_with_rows.json
+++ /dev/null
@@ -1,592 +0,0 @@
-{
-  "annotations": {
-    "list": [
-      {
-        "builtIn": 1,
-        "datasource": "-- Grafana --",
-        "enable": true,
-        "hide": true,
-        "iconColor": "rgba(0, 211, 255, 1)",
-        "name": "Annotations & Alerts",
-        "type": "dashboard"
-      }
-    ]
-  },
-  "editable": true,
-  "gnetId": null,
-  "graphTooltip": 0,
-  "id": 59,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 9,
-      "panels": [],
-      "title": "Row title",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 1
-      },
-      "id": 12,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 5,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 5
-      },
-      "id": 7,
-      "panels": [],
-      "title": "Row",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 6
-      },
-      "id": 2,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 6
-      },
-      "id": 13,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 10
-      },
-      "id": 11,
-      "panels": [],
-      "title": "Row title",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 11
-      },
-      "id": 4,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 11
-      },
-      "id": 3,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "tooltip": {
-        "shared": true,
-        "sort": 0,
-        "value_type": "individual"
-      },
-      "type": "graph",
-      "xaxis": {
-        "buckets": null,
-        "mode": "time",
-        "name": null,
-        "show": true,
-        "values": []
-      },
-      "yaxes": [
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        },
-        {
-          "format": "short",
-          "label": null,
-          "logBase": 1,
-          "max": null,
-          "min": null,
-          "show": true
-        }
-      ],
-      "yaxis": {
-        "align": false,
-        "alignLevel": null
-      }
-    }
-  ],
-  "schemaVersion": 16,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": []
-  },
-  "time": {
-    "from": "now-30m",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "",
-  "title": "Dashboard with rows",
-  "uid": "1DdOzBNmk",
-  "version": 5
-}
diff --git a/devenv/dev-dashboards/panel_tests_graph.json b/devenv/dev-dashboards/panel_tests_graph.json
new file mode 100644
index 00000000000..8a1770f0fa6
--- /dev/null
+++ b/devenv/dev-dashboards/panel_tests_graph.json
@@ -0,0 +1,1558 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 0
+      },
+      "id": 1,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "no_data_points",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "No Data Points Warning",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 0
+      },
+      "id": 2,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "datapoints_outside_range",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Datapoints Outside Range Warning",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 0
+      },
+      "id": 3,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "random_walk",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Random walk series",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 16,
+        "x": 0,
+        "y": 7
+      },
+      "id": 4,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "random_walk",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": "2s",
+      "timeShift": null,
+      "title": "Millisecond res x-axis and tooltip",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Just verify that the tooltip time has millisecond resolution ",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 7
+      },
+      "id": 6,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 9,
+        "w": 16,
+        "x": 0,
+        "y": 14
+      },
+      "id": 5,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "B-series",
+          "yaxis": 2
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "2000,3000,4000,1000,3000,10000",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "2 yaxis and axis labels",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "percent",
+          "label": "Perecent",
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": "Pressure",
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Verify that axis labels look ok",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 16,
+        "y": 14
+      },
+      "id": 7,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 23
+      },
+      "id": 8,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "null value connected",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 23
+      },
+      "id": 10,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null as zero",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "null value null as zero",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Should be a long line connecting the null region in the `connected`  mode, and in zero it should just be a line with zero value at the null points. ",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 23
+      },
+      "id": 13,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 16,
+        "x": 0,
+        "y": 30
+      },
+      "id": 9,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "B-series",
+          "zindex": -3
+        }
+      ],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "hide": false,
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Stacking value ontop of nulls",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Stacking values on top of nulls, should treat the null values as zero. ",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 30
+      },
+      "id": 14,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 16,
+        "x": 0,
+        "y": 37
+      },
+      "id": 12,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "B-series",
+          "zindex": -3
+        }
+      ],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        },
+        {
+          "alias": "",
+          "hide": false,
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Stacking all series null segment",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "content": "Stacking when all values are null should leave a gap in the graph",
+      "editable": true,
+      "error": false,
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 37
+      },
+      "id": 15,
+      "links": [],
+      "mode": "markdown",
+      "title": "",
+      "type": "text"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 24,
+        "x": 0,
+        "y": 44
+      },
+      "id": 20,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table Single Series Should Take Minimum Height",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 0,
+        "y": 51
+      },
+      "id": 16,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "D",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table No Scroll Visible",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 12,
+        "y": 51
+      },
+      "id": 17,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "D",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "E",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "F",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "G",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "H",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "I",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "J",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table Should Scroll",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 0,
+        "y": 58
+      },
+      "id": 18,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "D",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table No Scroll Visible",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 12,
+        "y": 58
+      },
+      "id": 19,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "C",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "D",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "E",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "F",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "G",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "H",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "I",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "J",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "K",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        },
+        {
+          "refId": "L",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Legend Table No Scroll Visible",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    }
+  ],
+  "refresh": false,
+  "revision": 8,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "gdev",
+    "panel-tests"
+  ],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "browser",
+  "title": "Panel Tests - Graph",
+  "uid": "5SdHCadmz",
+  "version": 3
+}
diff --git a/devenv/dev-dashboards/panel_tests_singlestat.json b/devenv/dev-dashboards/panel_tests_singlestat.json
new file mode 100644
index 00000000000..2d69f27bcb6
--- /dev/null
+++ b/devenv/dev-dashboards/panel_tests_singlestat.json
@@ -0,0 +1,574 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "postfix",
+      "postfixFontSize": "50%",
+      "prefix": "prefix",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": false,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "prefix 3 ms (green) postfixt + sparkline",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#d44a3a",
+        "rgba(237, 129, 40, 0.89)",
+        "#299c46"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 0
+      },
+      "id": 3,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "3 ms (red)  + full height sparkline",
+      "type": "singlestat",
+      "valueFontSize": "200%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": true,
+      "colorPrefix": false,
+      "colorValue": false,
+      "colors": [
+        "#d44a3a",
+        "rgba(237, 129, 40, 0.89)",
+        "#299c46"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 0
+      },
+      "id": 4,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "3 ms + red background",
+      "type": "singlestat",
+      "valueFontSize": "200%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": true,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 7
+      },
+      "id": 5,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 7
+      },
+      "id": 6,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90, no labels",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": false,
+        "thresholdMarkers": false
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 7
+      },
+      "id": 7,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90, no markers or labels",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    }
+  ],
+  "refresh": false,
+  "revision": 8,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "gdev",
+    "panel-tests"
+  ],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "browser",
+  "title": "Panel Tests - Singlestat",
+  "uid": "singlestat",
+  "version": 14
+}
diff --git a/devenv/dev-dashboards/panel_tests_table.json b/devenv/dev-dashboards/panel_tests_table.json
new file mode 100644
index 00000000000..8337e9cd746
--- /dev/null
+++ b/devenv/dev-dashboards/panel_tests_table.json
@@ -0,0 +1,453 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 11,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 3,
+      "links": [],
+      "pageSize": 10,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "server1",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "server2",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "title": "Time series to rows (2 pages)",
+      "transform": "timeseries_to_rows",
+      "type": "table"
+    },
+    {
+      "columns": [
+        {
+          "text": "Avg",
+          "value": "avg"
+        },
+        {
+          "text": "Max",
+          "value": "max"
+        },
+        {
+          "text": "Current",
+          "value": "current"
+        }
+      ],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 11,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 4,
+      "links": [],
+      "pageSize": 10,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "server1",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "server2",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "title": "Time series aggregations",
+      "transform": "timeseries_aggregations",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 7,
+        "w": 24,
+        "x": 0,
+        "y": 11
+      },
+      "id": 5,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "row",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "/Color/",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "ColorValue",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        }
+      ],
+      "title": "color row by threshold",
+      "transform": "timeseries_to_columns",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 18
+      },
+      "id": 2,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "ColorValue",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "ColorCell",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "5,1,2,3,4,5,10,20"
+        }
+      ],
+      "title": "Column style thresholds & units",
+      "transform": "timeseries_to_columns",
+      "type": "table"
+    }
+  ],
+  "refresh": false,
+  "revision": 8,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "gdev",
+    "panel-tests"
+  ],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "browser",
+  "title": "Panel Tests - Table",
+  "uid": "pttable",
+  "version": 1
+}
diff --git a/public/app/plugins/app/testdata/dashboards/alerts.json b/devenv/dev-dashboards/testdata_alerts.json
similarity index 98%
rename from public/app/plugins/app/testdata/dashboards/alerts.json
rename to devenv/dev-dashboards/testdata_alerts.json
index 159df0f458b..8c2edebf155 100644
--- a/public/app/plugins/app/testdata/dashboards/alerts.json
+++ b/devenv/dev-dashboards/testdata_alerts.json
@@ -1,6 +1,6 @@
 {
   "revision": 2,
-  "title": "TestData - Alerts",
+  "title": "Alerting with TestData",
   "tags": [
     "grafana-test"
   ],
@@ -48,7 +48,7 @@
           },
           "aliasColors": {},
           "bars": false,
-          "datasource": "Grafana TestData",
+          "datasource": "gdev-testdata",
           "editable": true,
           "error": false,
           "fill": 1,
@@ -161,7 +161,7 @@
           },
           "aliasColors": {},
           "bars": false,
-          "datasource": "Grafana TestData",
+          "datasource": "gdev-testdata",
           "editable": true,
           "error": false,
           "fill": 1,
diff --git a/devenv/setup.sh b/devenv/setup.sh
index 78dbfc1a366..6412bbc98ea 100755
--- a/devenv/setup.sh
+++ b/devenv/setup.sh
@@ -1,4 +1,4 @@
-#/bin/bash
+#!/bin/bash
 
 bulkDashboard() {
 
@@ -22,31 +22,37 @@ requiresJsonnet() {
 		fi
 }
 
-defaultDashboards() {
+devDashboards() {
+		echo -e "\xE2\x9C\x94 Setting up all dev dashboards using provisioning"
 		ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
 }
 
-defaultDatasources() {
-		echo "setting up all default datasources using provisioning"
+devDatasources() {
+		echo -e "\xE2\x9C\x94 Setting up all dev datasources using provisioning"
 
 		ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
 }
 
 usage() {
-	echo -e "install.sh\n\tThis script setups dev provision for datasources and dashboards"
+	echo -e "\n"
 	echo "Usage:"
 	echo "  bulk-dashboards                     - create and provisioning 400 dashboards"
 	echo "  no args                             - provisiong core datasources and dev dashboards"
 }
 
 main() {
+	echo -e "------------------------------------------------------------------"
+	echo -e "This script setups provisioning for dev datasources and dashboards"
+	echo -e "------------------------------------------------------------------"
+	echo -e "\n"
+
 	local cmd=$1
 
 	if [[ $cmd == "bulk-dashboards" ]]; then
 		bulkDashboard
 	else
-		defaultDashboards
-		defaultDatasources
+		devDashboards
+		devDatasources
 	fi
 
   if [[ -z "$cmd" ]]; then
diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go
index a1ab250ad37..c2c2ea3f696 100644
--- a/pkg/tsdb/testdata/testdata.go
+++ b/pkg/tsdb/testdata/testdata.go
@@ -21,7 +21,7 @@ func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, err
 }
 
 func init() {
-	tsdb.RegisterTsdbQueryEndpoint("grafana-testdata-datasource", NewTestDataExecutor)
+	tsdb.RegisterTsdbQueryEndpoint("testdata", NewTestDataExecutor)
 }
 
 func (e *TestDataExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts
index 6998321dd75..656ce2bfa38 100644
--- a/public/app/features/plugins/built_in_plugins.ts
+++ b/public/app/features/plugins/built_in_plugins.ts
@@ -9,6 +9,7 @@ import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
 import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
 import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
 import * as mssqlPlugin from 'app/plugins/datasource/mssql/module';
+import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
 
 import * as textPanel from 'app/plugins/panel/text/module';
 import * as graphPanel from 'app/plugins/panel/graph/module';
@@ -20,9 +21,6 @@ import * as tablePanel from 'app/plugins/panel/table/module';
 import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
 import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
 
-import * as testDataAppPlugin from 'app/plugins/app/testdata/module';
-import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module';
-
 const builtInPlugins = {
   'app/plugins/datasource/graphite/module': graphitePlugin,
   'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
@@ -35,8 +33,7 @@ const builtInPlugins = {
   'app/plugins/datasource/postgres/module': postgresPlugin,
   'app/plugins/datasource/mssql/module': mssqlPlugin,
   'app/plugins/datasource/prometheus/module': prometheusPlugin,
-  'app/plugins/app/testdata/module': testDataAppPlugin,
-  'app/plugins/app/testdata/datasource/module': testDataDSPlugin,
+  'app/plugins/datasource/testdata/module': testDataDSPlugin,
 
   'app/plugins/panel/text/module': textPanel,
   'app/plugins/panel/graph/module': graphPanel,
diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json
deleted file mode 100644
index 5a4459cd62c..00000000000
--- a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json
+++ /dev/null
@@ -1,1448 +0,0 @@
-{
-  "annotations": {
-    "list": []
-  },
-  "editable": true,
-  "gnetId": null,
-  "graphTooltip": 0,
-  "hideControls": false,
-  "links": [],
-  "refresh": false,
-  "revision": 8,
-  "rows": [
-    {
-      "collapse": false,
-      "height": "250px",
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 1,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 4,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenario": "random_walk",
-              "scenarioId": "no_data_points",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "No Data Points Warning",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 2,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 4,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenario": "random_walk",
-              "scenarioId": "datapoints_outside_range",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Datapoints Outside Range Warning",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 3,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 4,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenario": "random_walk",
-              "scenarioId": "random_walk",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Random walk series",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "New row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": "250px",
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 4,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 8,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenario": "random_walk",
-              "scenarioId": "random_walk",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": "2s",
-          "timeShift": null,
-          "title": "Millisecond res x-axis and tooltip",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "content": "Just verify that the tooltip time has millisecond resolution ",
-          "editable": true,
-          "error": false,
-          "id": 6,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "New row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 336,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 5,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [
-            {
-              "alias": "B-series",
-              "yaxis": 2
-            }
-          ],
-          "span": 8,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "2000,3000,4000,1000,3000,10000",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "2 yaxis and axis labels",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "percent",
-              "label": "Perecent",
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": "Pressure",
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "content": "Verify that axis labels look ok",
-          "editable": true,
-          "error": false,
-          "id": 7,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "New row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": "250px",
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 8,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 4,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "null value connected",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 10,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "null as zero",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 4,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "null value null as zero",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "content": "Should be a long line connecting the null region in the `connected`  mode, and in zero it should just be a line with zero value at the null points. ",
-          "editable": true,
-          "error": false,
-          "id": 13,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "New row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 9,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "null",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [
-            {
-              "alias": "B-series",
-              "zindex": -3
-            }
-          ],
-          "span": 8,
-          "stack": true,
-          "steppedLine": false,
-          "targets": [
-            {
-              "hide": false,
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10",
-              "target": ""
-            },
-            {
-              "alias": "",
-              "hide": false,
-              "refId": "A",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20",
-              "target": ""
-            },
-            {
-              "alias": "",
-              "hide": false,
-              "refId": "C",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Stacking value ontop of nulls",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "content": "Stacking values on top of nulls, should treat the null values as zero. ",
-          "editable": true,
-          "error": false,
-          "id": 14,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 12,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "null",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [
-            {
-              "alias": "B-series",
-              "zindex": -3
-            }
-          ],
-          "span": 8,
-          "stack": true,
-          "steppedLine": false,
-          "targets": [
-            {
-              "alias": "",
-              "hide": false,
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
-              "target": ""
-            },
-            {
-              "alias": "",
-              "hide": false,
-              "refId": "A",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
-              "target": ""
-            },
-            {
-              "alias": "",
-              "hide": false,
-              "refId": "C",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Stacking all series null segment",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "content": "Stacking when all values are null should leave a gap in the graph",
-          "editable": true,
-          "error": false,
-          "id": 15,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "id": 20,
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "max": true,
-            "min": true,
-            "show": true,
-            "total": true,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 1,
-          "links": [],
-          "nullPointMode": "null",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 12,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Legend Table Single Series Should Take Minimum Height",
-          "tooltip": {
-            "shared": true,
-            "sort": 0,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "id": 16,
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "max": true,
-            "min": true,
-            "show": true,
-            "total": true,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 1,
-          "links": [],
-          "nullPointMode": "null",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 6,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "C",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "D",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Legend Table No Scroll Visible",
-          "tooltip": {
-            "shared": true,
-            "sort": 0,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "id": 17,
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "max": true,
-            "min": true,
-            "show": true,
-            "total": true,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 1,
-          "links": [],
-          "nullPointMode": "null",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 6,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "C",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "D",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "E",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "F",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "G",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "H",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "I",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "J",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Legend Table Should Scroll",
-          "tooltip": {
-            "shared": true,
-            "sort": 0,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "id": 18,
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "max": true,
-            "min": true,
-            "rightSide": true,
-            "show": true,
-            "total": true,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 1,
-          "links": [],
-          "nullPointMode": "null",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 6,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "C",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "D",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Legend Table No Scroll Visible",
-          "tooltip": {
-            "shared": true,
-            "sort": 0,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "id": 19,
-          "legend": {
-            "alignAsTable": true,
-            "avg": true,
-            "current": true,
-            "max": true,
-            "min": true,
-            "rightSide": true,
-            "show": true,
-            "total": true,
-            "values": true
-          },
-          "lines": true,
-          "linewidth": 1,
-          "links": [],
-          "nullPointMode": "null",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 6,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "B",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "C",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "D",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "E",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "F",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "G",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "H",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "I",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "J",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "K",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            },
-            {
-              "refId": "L",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            }
-          ],
-          "thresholds": [],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Legend Table No Scroll Visible",
-          "tooltip": {
-            "shared": true,
-            "sort": 0,
-            "value_type": "individual"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    }
-  ],
-  "schemaVersion": 14,
-  "style": "dark",
-  "tags": [
-    "grafana-test"
-  ],
-  "templating": {
-    "list": []
-  },
-  "time": {
-    "from": "now-1h",
-    "to": "now"
-  },
-  "timepicker": {
-    "refresh_intervals": [
-      "5s",
-      "10s",
-      "30s",
-      "1m",
-      "5m",
-      "15m",
-      "30m",
-      "1h",
-      "2h",
-      "1d"
-    ],
-    "time_options": [
-      "5m",
-      "15m",
-      "1h",
-      "6h",
-      "12h",
-      "24h",
-      "2d",
-      "7d",
-      "30d"
-    ]
-  },
-  "timezone": "browser",
-  "title": "TestData - Graph Panel Last 1h",
-  "version": 2
-}
diff --git a/public/app/plugins/app/testdata/module.ts b/public/app/plugins/app/testdata/module.ts
deleted file mode 100644
index 812aba20464..00000000000
--- a/public/app/plugins/app/testdata/module.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-export class ConfigCtrl {
-  static template = '';
-
-  appEditCtrl: any;
-
-  /** @ngInject **/
-  constructor(private backendSrv) {
-    this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this));
-  }
-
-  initDatasource() {
-    return this.backendSrv.get('/api/datasources').then(res => {
-      var found = false;
-      for (let ds of res) {
-        if (ds.type === 'grafana-testdata-datasource') {
-          found = true;
-        }
-      }
-
-      if (!found) {
-        var dsInstance = {
-          name: 'Grafana TestData',
-          type: 'grafana-testdata-datasource',
-          access: 'direct',
-          jsonData: {},
-        };
-
-        return this.backendSrv.post('/api/datasources', dsInstance);
-      }
-
-      return Promise.resolve();
-    });
-  }
-}
diff --git a/public/app/plugins/app/testdata/plugin.json b/public/app/plugins/app/testdata/plugin.json
deleted file mode 100644
index 3efcd687453..00000000000
--- a/public/app/plugins/app/testdata/plugin.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
-  "type": "app",
-  "name": "Grafana TestData",
-  "id": "testdata",
-
-  "info": {
-    "description": "Grafana test data app",
-    "author": {
-      "name": "Grafana Project",
-      "url": "https://grafana.com"
-    },
-    "version": "1.0.17",
-    "updated": "2016-09-26"
-  },
-
-  "includes": [
-    {
-      "type": "dashboard",
-      "name": "TestData - Graph Last 1h",
-      "path": "dashboards/graph_last_1h.json"
-    },
-    {
-      "type": "dashboard",
-      "name": "TestData - Alerts",
-      "path": "dashboards/alerts.json"
-    }
-  ],
-
-  "dependencies": {
-    "grafanaVersion": "4.x.x"
-  }
-}
diff --git a/public/app/plugins/app/testdata/datasource/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts
similarity index 100%
rename from public/app/plugins/app/testdata/datasource/datasource.ts
rename to public/app/plugins/datasource/testdata/datasource.ts
diff --git a/public/app/plugins/app/testdata/datasource/module.ts b/public/app/plugins/datasource/testdata/module.ts
similarity index 100%
rename from public/app/plugins/app/testdata/datasource/module.ts
rename to public/app/plugins/datasource/testdata/module.ts
diff --git a/public/app/plugins/app/testdata/partials/query.editor.html b/public/app/plugins/datasource/testdata/partials/query.editor.html
similarity index 99%
rename from public/app/plugins/app/testdata/partials/query.editor.html
rename to public/app/plugins/datasource/testdata/partials/query.editor.html
index 247918bce1f..fc16f2a8b44 100644
--- a/public/app/plugins/app/testdata/partials/query.editor.html
+++ b/public/app/plugins/datasource/testdata/partials/query.editor.html
@@ -37,4 +37,3 @@
 		</div>
 	</div>
 </query-editor-row>
-
diff --git a/public/app/plugins/app/testdata/datasource/plugin.json b/public/app/plugins/datasource/testdata/plugin.json
similarity index 60%
rename from public/app/plugins/app/testdata/datasource/plugin.json
rename to public/app/plugins/datasource/testdata/plugin.json
index 80445dfb3bc..774603982e0 100644
--- a/public/app/plugins/app/testdata/datasource/plugin.json
+++ b/public/app/plugins/datasource/testdata/plugin.json
@@ -1,7 +1,7 @@
 {
   "type": "datasource",
-  "name": "Grafana TestDataDB",
-  "id": "grafana-testdata-datasource",
+  "name": "TestData DB",
+  "id": "testdata",
 
   "metrics": true,
   "alerting": true,
@@ -13,8 +13,8 @@
       "url": "https://grafana.com"
     },
     "logos": {
-      "small": "",
-      "large": ""
+      "small": "../../../../img/grafana_icon.svg",
+      "large": "../../../../img/grafana_icon.svg"
     }
   }
 }
diff --git a/public/app/plugins/app/testdata/datasource/query_ctrl.ts b/public/app/plugins/datasource/testdata/query_ctrl.ts
similarity index 100%
rename from public/app/plugins/app/testdata/datasource/query_ctrl.ts
rename to public/app/plugins/datasource/testdata/query_ctrl.ts
diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html
index 96576fd3c41..dd9ca55a760 100644
--- a/public/app/plugins/panel/singlestat/editor.html
+++ b/public/app/plugins/panel/singlestat/editor.html
@@ -56,10 +56,10 @@
     <h5 class="section-heading">Coloring</h5>
     <div class="gf-form-inline">
       <gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
-      <gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
+      <gf-form-switch class="gf-form" label-class="width-6" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
     </div>
     <div class="gf-form-inline">
-      <gf-form-switch class="gf-form" label-class="width-6" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
+      <gf-form-switch class="gf-form" label-class="width-8" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
       <gf-form-switch class="gf-form" label-class="width-6" label="Postfix" checked="ctrl.panel.colorPostfix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
     </div>
     <div class="gf-form-inline">

From 09c3569caaf7849982883c8d62ce4879c5b8f725 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 16 Jul 2018 12:36:35 +0200
Subject: [PATCH 022/380] Update README.md

---
 devenv/README.md | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/devenv/README.md b/devenv/README.md
index 4ec6f672f25..9abf3596776 100644
--- a/devenv/README.md
+++ b/devenv/README.md
@@ -1,11 +1,16 @@
 This folder contains useful scripts and configuration for...
 
-* Configuring datasources in Grafana
-* Provision example dashboards in Grafana
-* Run preconfiured datasources as docker containers
-
-want to know more? run setup!
+* Configuring dev datasources in Grafana
+* Configuring dev & test scenarios dashboards.
 
 ```bash
 ./setup.sh
 ```
+
+After restarting grafana server there should now be a number of datasources named `gdev-<type>` provisioned as well as a dashboard folder named `gdev dashboards`. This folder contains dashboard & panel features tests dashboards. 
+
+# Dev dashboards
+
+Please update these dashboards or make new ones as new panels & dashboards features are developed or new bugs are found. The dashboards are located in the `devenv/dev-dashboards` folder. 
+
+

From 1efe34e6cf5ffa0e604be526169ff10c3b5dadba Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Mon, 16 Jul 2018 12:40:55 +0200
Subject: [PATCH 023/380] Make prometheus value formatting more robust

- prometheus datasources passes its own interpolator function to the
  template server
- that function relies on incoming values being strings
- some template variables may be non-strings, e.g., `__interval_ms`,
  which throws an error

This PR makes this more robust.
---
 public/app/plugins/datasource/prometheus/datasource.ts | 10 ++++++++--
 .../datasource/prometheus/specs/datasource.jest.ts     |  3 +++
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 9ccda65a145..35b04066552 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -17,11 +17,17 @@ export function alignRange(start, end, step) {
 }
 
 export function prometheusRegularEscape(value) {
-  return value.replace(/'/g, "\\\\'");
+  if (typeof value === 'string') {
+    return value.replace(/'/g, "\\\\'");
+  }
+  return value;
 }
 
 export function prometheusSpecialRegexEscape(value) {
-  return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
+  if (typeof value === 'string') {
+    return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
+  }
+  return value;
 }
 
 export class PrometheusDatasource {
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index 219b990e5dd..15798a33cd2 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -166,6 +166,9 @@ describe('PrometheusDatasource', () => {
   });
 
   describe('Prometheus regular escaping', function() {
+    it('should not escape non-string', function() {
+      expect(prometheusRegularEscape(12)).toEqual(12);
+    });
     it('should not escape simple string', function() {
       expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
     });

From 0b210a6f5d1d6b3e967854607444e852fc33fef1 Mon Sep 17 00:00:00 2001
From: Daniel Lee <dan.limerick@gmail.com>
Date: Mon, 16 Jul 2018 12:46:51 +0200
Subject: [PATCH 024/380] ldap: docker block readme update

---
 docker/blocks/openldap/ldap_dev.toml | 85 ++++++++++++++++++++++++++++
 docker/blocks/openldap/notes.md      |  7 +--
 2 files changed, 87 insertions(+), 5 deletions(-)
 create mode 100644 docker/blocks/openldap/ldap_dev.toml

diff --git a/docker/blocks/openldap/ldap_dev.toml b/docker/blocks/openldap/ldap_dev.toml
new file mode 100644
index 00000000000..e79771b57de
--- /dev/null
+++ b/docker/blocks/openldap/ldap_dev.toml
@@ -0,0 +1,85 @@
+# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
+# [log]
+# filters = ldap:debug
+
+[[servers]]
+# Ldap server host (specify multiple hosts space separated)
+host = "127.0.0.1"
+# Default port is 389 or 636 if use_ssl = true
+port = 389
+# Set to true if ldap server supports TLS
+use_ssl = false
+# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
+start_tls = false
+# set to true if you want to skip ssl cert validation
+ssl_skip_verify = false
+# set to the path to your root CA certificate or leave unset to use system defaults
+# root_ca_cert = "/path/to/certificate.crt"
+
+# Search user bind dn
+bind_dn = "cn=admin,dc=grafana,dc=org"
+# Search user bind password
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+bind_password = 'grafana'
+
+# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
+search_filter = "(cn=%s)"
+
+# An array of base dns to search through
+search_base_dns = ["dc=grafana,dc=org"]
+
+# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
+# This is done by enabling group_search_filter below. You must also set member_of= "cn"
+# in [servers.attributes] below.
+
+# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
+# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
+# below in such a way that the user's recursive group membership is considered.
+#
+# Nested Groups + Active Directory (AD) Example:
+#
+#   AD groups store the Distinguished Names (DNs) of members, so your filter must
+#   recursively search your groups for the authenticating user's DN. For example:
+#
+#     group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
+#     group_search_filter_user_attribute = "distinguishedName"
+#     group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+#
+#     [servers.attributes]
+#     ...
+#     member_of = "distinguishedName"
+
+## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
+# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
+## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
+## Defaults to the value of username in [server.attributes]
+## Valid options are any of your values in [servers.attributes]
+## If you are using nested groups you probably want to set this and member_of in
+## [servers.attributes] to "distinguishedName"
+# group_search_filter_user_attribute = "distinguishedName"
+## An array of the base DNs to search through for groups. Typically uses ou=groups
+# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+
+# Specify names of the ldap attributes your ldap uses
+[servers.attributes]
+name = "givenName"
+surname = "sn"
+username = "cn"
+member_of = "memberOf"
+email =  "email"
+
+# Map ldap groups to grafana org roles
+[[servers.group_mappings]]
+group_dn = "cn=admins,ou=groups,dc=grafana,dc=org"
+org_role = "Admin"
+# The Grafana organization database id, optional, if left out the default org (id 1) will be used
+# org_id = 1
+
+[[servers.group_mappings]]
+group_dn = "cn=editors,ou=groups,dc=grafana,dc=org"
+org_role = "Editor"
+
+[[servers.group_mappings]]
+# If you want to match all (or no ldap groups) then you can use wildcard
+group_dn = "*"
+org_role = "Viewer"
diff --git a/docker/blocks/openldap/notes.md b/docker/blocks/openldap/notes.md
index 8de23d5ccf2..65155423616 100644
--- a/docker/blocks/openldap/notes.md
+++ b/docker/blocks/openldap/notes.md
@@ -14,12 +14,12 @@ After adding ldif files to `prepopulate`:
 
 ## Enabling LDAP in Grafana
 
-The default `ldap.toml` file in `conf` has host set to `127.0.0.1` and port to set to 389 so all you need to do is enable it in the .ini file to get Grafana to use this block:
+Copy the ldap_dev.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block:
 
 ```ini
 [auth.ldap]
 enabled = true
-config_file = conf/ldap.toml
+config_file = conf/ldap_dev.toml
 ; allow_sign_up = true
 ```
 
@@ -43,6 +43,3 @@ editors
 
 no groups
   ldap-viewer
-
-
-

From 1f74b298c4ee3f5777f9b10d5ce5f6da8f4fc8ac Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Mon, 16 Jul 2018 13:02:36 +0200
Subject: [PATCH 025/380] Remove string casting for template variables in
 prometheus

---
 .../app/features/panel/metrics_panel_ctrl.ts  |  2 +-
 .../datasource/prometheus/datasource.ts       |  2 +-
 .../prometheus/specs/datasource_specs.ts      | 36 +++++++++----------
 3 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts
index 6eb6d3b3b00..75c0de3bc6e 100644
--- a/public/app/features/panel/metrics_panel_ctrl.ts
+++ b/public/app/features/panel/metrics_panel_ctrl.ts
@@ -222,7 +222,7 @@ class MetricsPanelCtrl extends PanelCtrl {
     // and add built in variables interval and interval_ms
     var scopedVars = Object.assign({}, this.panel.scopedVars, {
       __interval: { text: this.interval, value: this.interval },
-      __interval_ms: { text: String(this.intervalMs), value: String(this.intervalMs) },
+      __interval_ms: { text: this.intervalMs, value: this.intervalMs },
     });
 
     var metricsQuery = {
diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 35b04066552..69ce6f440c5 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -202,7 +202,7 @@ export class PrometheusDatasource {
       interval = adjustedInterval;
       scopedVars = Object.assign({}, options.scopedVars, {
         __interval: { text: interval + 's', value: interval + 's' },
-        __interval_ms: { text: String(interval * 1000), value: String(interval * 1000) },
+        __interval_ms: { text: interval * 1000, value: interval * 1000 },
       });
     }
     query.step = interval;
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
index 09aa934dd63..c5da671b757 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
@@ -452,7 +452,7 @@ describe('PrometheusDatasource', function() {
         interval: '10s',
         scopedVars: {
           __interval: { text: '10s', value: '10s' },
-          __interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
+          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
         },
       };
       var urlExpected =
@@ -463,8 +463,8 @@ describe('PrometheusDatasource', function() {
 
       expect(query.scopedVars.__interval.text).to.be('10s');
       expect(query.scopedVars.__interval.value).to.be('10s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
     });
     it('should be min interval when it is greater than auto interval', function() {
       var query = {
@@ -479,7 +479,7 @@ describe('PrometheusDatasource', function() {
         interval: '5s',
         scopedVars: {
           __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
         },
       };
       var urlExpected =
@@ -490,8 +490,8 @@ describe('PrometheusDatasource', function() {
 
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
     });
     it('should account for intervalFactor', function() {
       var query = {
@@ -507,7 +507,7 @@ describe('PrometheusDatasource', function() {
         interval: '10s',
         scopedVars: {
           __interval: { text: '10s', value: '10s' },
-          __interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
+          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
         },
       };
       var urlExpected =
@@ -518,8 +518,8 @@ describe('PrometheusDatasource', function() {
 
       expect(query.scopedVars.__interval.text).to.be('10s');
       expect(query.scopedVars.__interval.value).to.be('10s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
     });
     it('should be interval * intervalFactor when greater than min interval', function() {
       var query = {
@@ -535,7 +535,7 @@ describe('PrometheusDatasource', function() {
         interval: '5s',
         scopedVars: {
           __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
         },
       };
       var urlExpected =
@@ -546,8 +546,8 @@ describe('PrometheusDatasource', function() {
 
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
     });
     it('should be min interval when greater than interval * intervalFactor', function() {
       var query = {
@@ -563,7 +563,7 @@ describe('PrometheusDatasource', function() {
         interval: '5s',
         scopedVars: {
           __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
         },
       };
       var urlExpected =
@@ -574,8 +574,8 @@ describe('PrometheusDatasource', function() {
 
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
     });
     it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
       var query = {
@@ -590,7 +590,7 @@ describe('PrometheusDatasource', function() {
         interval: '5s',
         scopedVars: {
           __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
         },
       };
       var end = 7 * 24 * 60 * 60;
@@ -609,8 +609,8 @@ describe('PrometheusDatasource', function() {
 
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
     });
   });
 });

From a1f0dffe011ca667e45b3b7b67d8da70179f8f2c Mon Sep 17 00:00:00 2001
From: Daniel Lee <dan.limerick@gmail.com>
Date: Mon, 16 Jul 2018 15:09:42 +0200
Subject: [PATCH 026/380] nginx: update to docker block

Adds commented out settings in nginx conf
for testing basic auth and auth proxy
---
 docker/blocks/nginx_proxy/Dockerfile |  3 ++-
 docker/blocks/nginx_proxy/htpasswd   |  3 +++
 docker/blocks/nginx_proxy/nginx.conf | 21 ++++++++++++++++++++-
 3 files changed, 25 insertions(+), 2 deletions(-)
 create mode 100755 docker/blocks/nginx_proxy/htpasswd

diff --git a/docker/blocks/nginx_proxy/Dockerfile b/docker/blocks/nginx_proxy/Dockerfile
index 9ded20dfdda..04de507499d 100644
--- a/docker/blocks/nginx_proxy/Dockerfile
+++ b/docker/blocks/nginx_proxy/Dockerfile
@@ -1,3 +1,4 @@
 FROM nginx:alpine
 
-COPY nginx.conf /etc/nginx/nginx.conf
\ No newline at end of file
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY htpasswd /etc/nginx/htpasswd
diff --git a/docker/blocks/nginx_proxy/htpasswd b/docker/blocks/nginx_proxy/htpasswd
new file mode 100755
index 00000000000..e2c5eeeff7b
--- /dev/null
+++ b/docker/blocks/nginx_proxy/htpasswd
@@ -0,0 +1,3 @@
+user1:$apr1$1odeeQb.$kwV8D/VAAGUDU7pnHuKoV0
+user2:$apr1$A2kf25r.$6S0kp3C7vIuixS5CL0XA9.
+admin:$apr1$IWn4DoRR$E2ol7fS/dkI18eU4bXnBO1
diff --git a/docker/blocks/nginx_proxy/nginx.conf b/docker/blocks/nginx_proxy/nginx.conf
index 18e27b3fb01..860d3d0b89f 100644
--- a/docker/blocks/nginx_proxy/nginx.conf
+++ b/docker/blocks/nginx_proxy/nginx.conf
@@ -13,7 +13,26 @@ http {
     listen 10080;
 
     location /grafana/ {
+      ################################################################
+      # Enable these settings to test with basic auth and an auth proxy header
+      # the htpasswd file contains an admin user with password admin and
+      # user1: grafana and user2: grafana
+      ################################################################
+
+      # auth_basic "Restricted Content";
+      # auth_basic_user_file /etc/nginx/htpasswd;
+
+      ################################################################
+      # To use the auth proxy header, set the following in custom.ini:
+      # [auth.proxy]
+      # enabled = true
+      # header_name = X-WEBAUTH-USER
+      # header_property = username
+      ################################################################
+
+      # proxy_set_header X-WEBAUTH-USER $remote_user;
+
       proxy_pass http://localhost:3000/;
     }
   }
-}
\ No newline at end of file
+}

From c189262bac09565c90467ff71c0ec44196fa0ba3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 16 Jul 2018 16:56:42 +0200
Subject: [PATCH 027/380] ldap: Make it possible to define Grafana admins via
 ldap setup, closes #2469

---
 conf/ldap.toml                    |  2 ++
 docs/sources/installation/ldap.md | 11 +++++--
 pkg/login/ext_user.go             |  7 +++++
 pkg/login/ldap.go                 |  7 +++--
 pkg/login/ldap_settings.go        |  7 +++--
 pkg/login/ldap_test.go            | 50 ++++++++++++++++++++++++++-----
 pkg/models/user_auth.go           | 17 ++++++-----
 7 files changed, 77 insertions(+), 24 deletions(-)

diff --git a/conf/ldap.toml b/conf/ldap.toml
index 166d85eabb1..1b207d8424e 100644
--- a/conf/ldap.toml
+++ b/conf/ldap.toml
@@ -72,6 +72,8 @@ email =  "email"
 [[servers.group_mappings]]
 group_dn = "cn=admins,dc=grafana,dc=org"
 org_role = "Admin"
+# To make user a instance admin  (Grafana Admin) uncomment line below
+# grafana_admin = true
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used
 # org_id = 1
 
diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md
index 85501e51d85..ce77a1124e9 100644
--- a/docs/sources/installation/ldap.md
+++ b/docs/sources/installation/ldap.md
@@ -23,8 +23,9 @@ specific configuration file (default: `/etc/grafana/ldap.toml`).
 ### Example config
 
 ```toml
-# Set to true to log user information returned from LDAP
-verbose_logging = false
+# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
+# [log]
+# filters = ldap:debug
 
 [[servers]]
 # Ldap server host (specify multiple hosts space separated)
@@ -73,6 +74,8 @@ email =  "email"
 [[servers.group_mappings]]
 group_dn = "cn=admins,dc=grafana,dc=org"
 org_role = "Admin"
+# To make user a instance admin  (Grafana Admin) uncomment line below
+# grafana_admin = true
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used.  Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
 # org_id = 1
 
@@ -132,6 +135,10 @@ Users page, this change will be reset the next time the user logs in. If you
 change the LDAP groups of a user, the change will take effect the next
 time the user logs in.
 
+### Grafana Admin
+with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs &
+users.
+
 ### Priority
 The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used.
 
diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go
index d6eaf9a975e..a421e3ebe0a 100644
--- a/pkg/login/ext_user.go
+++ b/pkg/login/ext_user.go
@@ -72,6 +72,13 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
 		return err
 	}
 
+	// Sync isGrafanaAdmin permission
+	if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
+		if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
+			return err
+		}
+	}
+
 	err = bus.Dispatch(&m.SyncTeamsCommand{
 		User:         cmd.Result,
 		ExternalUser: extUser,
diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go
index bdf87b2db54..9e4918f0290 100644
--- a/pkg/login/ldap.go
+++ b/pkg/login/ldap.go
@@ -175,6 +175,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 
 		if ldapUser.isMemberOf(group.GroupDN) {
 			extUser.OrgRoles[group.OrgId] = group.OrgRole
+			extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
 		}
 	}
 
@@ -190,18 +191,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 	}
 
 	// add/update user in grafana
-	userQuery := &m.UpsertUserCommand{
+	upsertUserCmd := &m.UpsertUserCommand{
 		ReqContext:    ctx,
 		ExternalUser:  extUser,
 		SignupAllowed: setting.LdapAllowSignup,
 	}
 
-	err := bus.Dispatch(userQuery)
+	err := bus.Dispatch(upsertUserCmd)
 	if err != nil {
 		return nil, err
 	}
 
-	return userQuery.Result, nil
+	return upsertUserCmd.Result, nil
 }
 
 func (a *ldapAuther) serverBind() error {
diff --git a/pkg/login/ldap_settings.go b/pkg/login/ldap_settings.go
index 497d8725e29..c4f5982b237 100644
--- a/pkg/login/ldap_settings.go
+++ b/pkg/login/ldap_settings.go
@@ -44,9 +44,10 @@ type LdapAttributeMap struct {
 }
 
 type LdapGroupToOrgRole struct {
-	GroupDN string     `toml:"group_dn"`
-	OrgId   int64      `toml:"org_id"`
-	OrgRole m.RoleType `toml:"org_role"`
+	GroupDN        string     `toml:"group_dn"`
+	OrgId          int64      `toml:"org_id"`
+	IsGrafanaAdmin *bool      `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatability)
+	OrgRole        m.RoleType `toml:"org_role"`
 }
 
 var LdapCfg LdapConfig
diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go
index 5080840704e..1cf98bd1e14 100644
--- a/pkg/login/ldap_test.go
+++ b/pkg/login/ldap_test.go
@@ -98,6 +98,10 @@ func TestLdapAuther(t *testing.T) {
 				So(result.Login, ShouldEqual, "torkelo")
 			})
 
+			Convey("Should set isGrafanaAdmin to false by default", func() {
+				So(result.IsAdmin, ShouldBeFalse)
+			})
+
 		})
 
 	})
@@ -223,8 +227,32 @@ func TestLdapAuther(t *testing.T) {
 				So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
 				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 			})
+
+			Convey("Should not update permissions unless specified", func() {
+				So(err, ShouldBeNil)
+				So(sc.updateUserPermissionsCmd, ShouldBeNil)
+			})
 		})
 
+		ldapAutherScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
+			trueVal := true
+
+			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
+				LdapGroups: []*LdapGroupToOrgRole{
+					{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
+				},
+			})
+
+			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
+				MemberOf: []string{"cn=admins"},
+			})
+
+			Convey("Should create user with admin set to true", func() {
+				So(err, ShouldBeNil)
+				So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
+			})
+		})
 	})
 
 	Convey("When calling SyncUser", t, func() {
@@ -332,6 +360,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 			return nil
 		})
 
+		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpdateUserPermissionsCommand) error {
+			sc.updateUserPermissionsCmd = cmd
+			return nil
+		})
+
 		bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
 			sc.getUserByAuthInfoQuery = cmd
 			sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
@@ -379,14 +412,15 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 }
 
 type scenarioContext struct {
-	getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
-	getUserOrgListQuery    *m.GetUserOrgListQuery
-	createUserCmd          *m.CreateUserCommand
-	addOrgUserCmd          *m.AddOrgUserCommand
-	updateOrgUserCmd       *m.UpdateOrgUserCommand
-	removeOrgUserCmd       *m.RemoveOrgUserCommand
-	updateUserCmd          *m.UpdateUserCommand
-	setUsingOrgCmd         *m.SetUsingOrgCommand
+	getUserByAuthInfoQuery   *m.GetUserByAuthInfoQuery
+	getUserOrgListQuery      *m.GetUserOrgListQuery
+	createUserCmd            *m.CreateUserCommand
+	addOrgUserCmd            *m.AddOrgUserCommand
+	updateOrgUserCmd         *m.UpdateOrgUserCommand
+	removeOrgUserCmd         *m.RemoveOrgUserCommand
+	updateUserCmd            *m.UpdateUserCommand
+	setUsingOrgCmd           *m.SetUsingOrgCommand
+	updateUserPermissionsCmd *m.UpdateUserPermissionsCommand
 }
 
 func (sc *scenarioContext) userQueryReturns(user *m.User) {
diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go
index 162a4d867a9..28189005737 100644
--- a/pkg/models/user_auth.go
+++ b/pkg/models/user_auth.go
@@ -13,14 +13,15 @@ type UserAuth struct {
 }
 
 type ExternalUserInfo struct {
-	AuthModule string
-	AuthId     string
-	UserId     int64
-	Email      string
-	Login      string
-	Name       string
-	Groups     []string
-	OrgRoles   map[int64]RoleType
+	AuthModule     string
+	AuthId         string
+	UserId         int64
+	Email          string
+	Login          string
+	Name           string
+	Groups         []string
+	OrgRoles       map[int64]RoleType
+	IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync)
 }
 
 // ---------------------

From 10aaf7b506009461f98de8f399f3e295c7c8dcfc Mon Sep 17 00:00:00 2001
From: Caleb Tote <caleb.tote@gmail.com>
Date: Mon, 16 Jul 2018 12:38:42 -0400
Subject: [PATCH 028/380] Adding eval_data to alerts query results

---
 pkg/services/sqlstore/alert.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go
index 531a70b2101..af911dc22e6 100644
--- a/pkg/services/sqlstore/alert.go
+++ b/pkg/services/sqlstore/alert.go
@@ -73,6 +73,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 		alert.name,
 		alert.state,
 		alert.new_state_date,
+		alert.eval_data,
 		alert.eval_date,
 		alert.execution_error,
 		dashboard.uid as dashboard_uid,

From e318489bd4bee15b59d8d1f222dcb6b433ee122a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 17 Jul 2018 11:56:33 +0200
Subject: [PATCH 029/380] Fix default browser th font-weight

---
 public/sass/base/_type.scss | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss
index 1c3516c2828..2de8665f06a 100644
--- a/public/sass/base/_type.scss
+++ b/public/sass/base/_type.scss
@@ -24,7 +24,7 @@ small {
   font-size: 85%;
 }
 strong {
-  font-weight: bold;
+  font-weight: $font-weight-semi-bold;
 }
 em {
   font-style: italic;
@@ -249,7 +249,7 @@ dd {
   line-height: $line-height-base;
 }
 dt {
-  font-weight: bold;
+  font-weight: $font-weight-semi-bold;
 }
 dd {
   margin-left: $line-height-base / 2;
@@ -376,7 +376,7 @@ a.external-link {
       padding: $spacer*0.5 $spacer;
     }
     th {
-      font-weight: normal;
+      font-weight: $font-weight-semi-bold;
       background: $table-bg-accent;
     }
   }
@@ -415,3 +415,7 @@ a.external-link {
   color: $yellow;
   padding: 0;
 }
+
+th {
+  font-weight: $font-weight-semi-bold;
+}

From f67b27e0092d07c1b28926932bdb1f54113d076e Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Tue, 17 Jul 2018 12:24:04 +0200
Subject: [PATCH 030/380] Dont parse empty explore state from url

- only parse url state if there is any
- prevents parse exception in the console on empty explore state
---
 public/app/containers/Explore/Explore.tsx | 24 ++++++++++++-----------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 81e1922d2cd..e393ce7bf88 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -31,18 +31,20 @@ function makeTimeSeriesList(dataList, options) {
   });
 }
 
-function parseInitialState(initial) {
-  try {
-    const parsed = JSON.parse(decodePathComponent(initial));
-    return {
-      datasource: parsed.datasource,
-      queries: parsed.queries.map(q => q.query),
-      range: parsed.range,
-    };
-  } catch (e) {
-    console.error(e);
-    return { queries: [], range: DEFAULT_RANGE };
+function parseInitialState(initial: string | undefined) {
+  if (initial) {
+    try {
+      const parsed = JSON.parse(decodePathComponent(initial));
+      return {
+        datasource: parsed.datasource,
+        queries: parsed.queries.map(q => q.query),
+        range: parsed.range,
+      };
+    } catch (e) {
+      console.error(e);
+    }
   }
+  return { datasource: null, queries: [], range: DEFAULT_RANGE };
 }
 
 interface IExploreState {

From c6e9ffb1689ccf991346e3bcb8bd1faf5f751107 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Tue, 17 Jul 2018 12:56:05 +0200
Subject: [PATCH 031/380] Use url params for explore state

- putting state in the path components led to 400 on reload
- use `/explore?state=JSON` instead
---
 pkg/api/api.go                                  | 3 +--
 public/app/containers/Explore/Explore.tsx       | 2 +-
 public/app/core/services/keybindingSrv.ts       | 2 +-
 public/app/features/panel/metrics_panel_ctrl.ts | 2 +-
 public/app/routes/routes.ts                     | 2 +-
 5 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/pkg/api/api.go b/pkg/api/api.go
index 8870b9b095e..84425fdae3d 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -73,8 +73,7 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Get("/dashboards/", reqSignedIn, Index)
 	r.Get("/dashboards/*", reqSignedIn, Index)
 
-	r.Get("/explore/", reqEditorRole, Index)
-	r.Get("/explore/*", reqEditorRole, Index)
+	r.Get("/explore", reqEditorRole, Index)
 
 	r.Get("/playlists/", reqSignedIn, Index)
 	r.Get("/playlists/*", reqSignedIn, Index)
diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 81e1922d2cd..6486a3a58c9 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -67,7 +67,7 @@ interface IExploreState {
 export class Explore extends React.Component<any, IExploreState> {
   constructor(props) {
     super(props);
-    const { datasource, queries, range } = parseInitialState(props.routeParams.initial);
+    const { datasource, queries, range } = parseInitialState(props.routeParams.state);
     this.state = {
       datasource: null,
       datasourceError: null,
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index cbc7871fbbd..672ae29740b 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -191,7 +191,7 @@ export class KeybindingSrv {
               range,
             };
             const exploreState = encodePathComponent(JSON.stringify(state));
-            this.$location.url(`/explore/${exploreState}`);
+            this.$location.url(`/explore?state=${exploreState}`);
           }
         }
       });
diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts
index 6eb6d3b3b00..3567abf948b 100644
--- a/public/app/features/panel/metrics_panel_ctrl.ts
+++ b/public/app/features/panel/metrics_panel_ctrl.ts
@@ -332,7 +332,7 @@ class MetricsPanelCtrl extends PanelCtrl {
       range,
     };
     const exploreState = encodePathComponent(JSON.stringify(state));
-    this.$location.url(`/explore/${exploreState}`);
+    this.$location.url(`/explore?state=${exploreState}`);
   }
 
   addQuery(target) {
diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts
index cd1aed549e0..d12711aca5b 100644
--- a/public/app/routes/routes.ts
+++ b/public/app/routes/routes.ts
@@ -112,7 +112,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controller: 'FolderDashboardsCtrl',
       controllerAs: 'ctrl',
     })
-    .when('/explore/:initial?', {
+    .when('/explore', {
       template: '<react-container />',
       resolve: {
         roles: () => ['Editor', 'Admin'],

From 02427ef88d3bff05c983314f4f7588485301d6ae Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Tue, 17 Jul 2018 15:13:44 +0200
Subject: [PATCH 032/380] Explore: calculate query interval based on available
 width

- classic dashboard panels inject a dynamic query interval as part of
  the query options. Explore did not have that.
- this PR adds the interval calculation to Explore
- interval based on Explore container's width
- ensure min interval if set in datasource
---
 public/app/containers/Explore/Explore.tsx    | 49 +++++++++++++-------
 public/app/containers/Explore/utils/query.ts | 12 -----
 2 files changed, 33 insertions(+), 28 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 81e1922d2cd..5ebde6e853f 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -2,16 +2,18 @@ import React from 'react';
 import { hot } from 'react-hot-loader';
 import Select from 'react-select';
 
+import kbn from 'app/core/utils/kbn';
 import colors from 'app/core/utils/colors';
 import TimeSeries from 'app/core/time_series2';
 import { decodePathComponent } from 'app/core/utils/location_util';
+import { parse as parseDate } from 'app/core/utils/datemath';
 
 import ElapsedTime from './ElapsedTime';
 import QueryRows from './QueryRows';
 import Graph from './Graph';
 import Table from './Table';
 import TimePicker, { DEFAULT_RANGE } from './TimePicker';
-import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
+import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
 
 function makeTimeSeriesList(dataList, options) {
   return dataList.map((seriesData, index) => {
@@ -63,8 +65,9 @@ interface IExploreState {
   tableResult: any;
 }
 
-// @observer
 export class Explore extends React.Component<any, IExploreState> {
+  el: any;
+
   constructor(props) {
     super(props);
     const { datasource, queries, range } = parseInitialState(props.routeParams.initial);
@@ -132,6 +135,10 @@ export class Explore extends React.Component<any, IExploreState> {
     }
   }
 
+  getRef = el => {
+    this.el = el;
+  };
+
   handleAddQueryRow = index => {
     const { queries } = this.state;
     const nextQueries = [
@@ -214,20 +221,33 @@ export class Explore extends React.Component<any, IExploreState> {
     }
   };
 
-  async runGraphQuery() {
+  buildQueryOptions(targetOptions: { format: string; instant: boolean }) {
     const { datasource, queries, range } = this.state;
+    const resolution = this.el.offsetWidth;
+    const absoluteRange = {
+      from: parseDate(range.from, false),
+      to: parseDate(range.to, true),
+    };
+    const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
+    const targets = queries.map(q => ({
+      ...targetOptions,
+      expr: q.query,
+    }));
+    return {
+      interval,
+      range,
+      targets,
+    };
+  }
+
+  async runGraphQuery() {
+    const { datasource, queries } = this.state;
     if (!hasQuery(queries)) {
       return;
     }
     this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
     const now = Date.now();
-    const options = buildQueryOptions({
-      format: 'time_series',
-      interval: datasource.interval,
-      instant: false,
-      range,
-      queries: queries.map(q => q.query),
-    });
+    const options = this.buildQueryOptions({ format: 'time_series', instant: false });
     try {
       const res = await datasource.query(options);
       const result = makeTimeSeriesList(res.data, options);
@@ -241,18 +261,15 @@ export class Explore extends React.Component<any, IExploreState> {
   }
 
   async runTableQuery() {
-    const { datasource, queries, range } = this.state;
+    const { datasource, queries } = this.state;
     if (!hasQuery(queries)) {
       return;
     }
     this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
     const now = Date.now();
-    const options = buildQueryOptions({
+    const options = this.buildQueryOptions({
       format: 'table',
-      interval: datasource.interval,
       instant: true,
-      range,
-      queries: queries.map(q => q.query),
     });
     try {
       const res = await datasource.query(options);
@@ -301,7 +318,7 @@ export class Explore extends React.Component<any, IExploreState> {
     const selectedDatasource = datasource ? datasource.name : undefined;
 
     return (
-      <div className={exploreClass}>
+      <div className={exploreClass} ref={this.getRef}>
         <div className="navbar">
           {position === 'left' ? (
             <div>
diff --git a/public/app/containers/Explore/utils/query.ts b/public/app/containers/Explore/utils/query.ts
index 3aa0cc5b357..d774f619a30 100644
--- a/public/app/containers/Explore/utils/query.ts
+++ b/public/app/containers/Explore/utils/query.ts
@@ -1,15 +1,3 @@
-export function buildQueryOptions({ format, interval, instant, range, queries }) {
-  return {
-    interval,
-    range,
-    targets: queries.map(expr => ({
-      expr,
-      format,
-      instant,
-    })),
-  };
-}
-
 export function generateQueryKey(index = 0) {
   return `Q-${Date.now()}-${Math.random()}-${index}`;
 }

From 05e060dee0f228e9a2beaee9f4fc81cf0cecfb70 Mon Sep 17 00:00:00 2001
From: Augustin <Nexucis@users.noreply.github.com>
Date: Tue, 17 Jul 2018 16:45:39 +0200
Subject: [PATCH 033/380] HTTP API documentation +fix when updating a playlist
 (#12612)

* get id from path param when updating a playlist

* add playlist http api documentation

* remove required condition for the id in update cmd
---
 docs/sources/http_api/playlist.md | 286 ++++++++++++++++++++++++++++++
 pkg/api/playlist.go               |   1 +
 pkg/models/playlist.go            |   2 +-
 3 files changed, 288 insertions(+), 1 deletion(-)
 create mode 100644 docs/sources/http_api/playlist.md

diff --git a/docs/sources/http_api/playlist.md b/docs/sources/http_api/playlist.md
new file mode 100644
index 00000000000..7c33900969b
--- /dev/null
+++ b/docs/sources/http_api/playlist.md
@@ -0,0 +1,286 @@
++++
+title = "Playlist HTTP API "
+description = "Playlist Admin HTTP API"
+keywords = ["grafana", "http", "documentation", "api", "playlist"]
+aliases = ["/http_api/playlist/"]
+type = "docs"
+[menu.docs]
+name = "Playlist"
+parent = "http_api"
++++
+
+# Playlist API
+
+## Search Playlist
+
+`GET /api/playlists`
+
+Get all existing playlist for the current organization using pagination
+
+**Example Request**:
+
+```bash
+GET /api/playlists HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+  Querystring Parameters:
+
+  These parameters are used as querystring parameters.
+  
+  - **query** - Limit response to playlist having a name like this value.
+  - **limit** - Limit response to *X* number of playlist.
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 1,
+    "name": "my playlist",
+    "interval": "5m"
+  }
+]
+```
+
+## Get one playlist
+
+`GET /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1 HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{
+  "id" : 1,
+  "name": "my playlist",
+  "interval": "5m",
+  "orgId": "my org",
+  "items": [
+    {
+      "id": 1,
+      "playlistId": 1,
+      "type": "dashboard_by_id",
+      "value": "3",
+      "order": 1,
+      "title":"my third dasboard"
+    },
+    {
+      "id": 2,
+      "playlistId": 1,
+      "type": "dashboard_by_tag",
+      "value": "myTag",
+      "order": 2,
+      "title":"my other dasboard"
+    }
+  ]
+}
+```
+
+## Get Playlist items
+
+`GET /api/playlists/:id/items`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1/items HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 1,
+    "playlistId": 1,
+    "type": "dashboard_by_id",
+    "value": "3",
+    "order": 1,
+    "title":"my third dasboard"
+  },
+  {
+    "id": 2,
+    "playlistId": 1,
+    "type": "dashboard_by_tag",
+    "value": "myTag",
+    "order": 2,
+    "title":"my other dasboard"
+  }
+]
+```
+
+## Get Playlist dashboards
+
+`GET /api/playlists/:id/dashboards`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1/dashboards HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 3,
+    "title": "my third dasboard",
+    "order": 1,
+  },
+  {
+    "id": 5,
+    "title":"my other dasboard"
+    "order": 2,
+    
+  }
+]
+```
+
+## Create a playlist
+
+`POST /api/playlists/`
+
+**Example Request**:
+
+```bash
+PUT /api/playlists/1 HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+  {
+    "name": "my playlist",
+    "interval": "5m",
+    "items": [
+      {
+        "type": "dashboard_by_id",
+        "value": "3",
+        "order": 1,
+        "title":"my third dasboard"
+      },
+      {
+        "type": "dashboard_by_tag",
+        "value": "myTag",
+        "order": 2,
+        "title":"my other dasboard"
+      }
+    ]
+  }
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+  {
+    "id": 1,
+    "name": "my playlist",
+    "interval": "5m"
+  }
+```
+
+## Update a playlist
+
+`PUT /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+PUT /api/playlists/1 HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+  {
+    "name": "my playlist",
+    "interval": "5m",
+    "items": [
+      {
+        "playlistId": 1,
+        "type": "dashboard_by_id",
+        "value": "3",
+        "order": 1,
+        "title":"my third dasboard"
+      },
+      {
+        "playlistId": 1,
+        "type": "dashboard_by_tag",
+        "value": "myTag",
+        "order": 2,
+        "title":"my other dasboard"
+      }
+    ]
+  }
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{
+  "id" : 1,
+  "name": "my playlist",
+  "interval": "5m",
+  "orgId": "my org",
+  "items": [
+    {
+      "id": 1,
+      "playlistId": 1,
+      "type": "dashboard_by_id",
+      "value": "3",
+      "order": 1,
+      "title":"my third dasboard"
+    },
+    {
+      "id": 2,
+      "playlistId": 1,
+      "type": "dashboard_by_tag",
+      "value": "myTag",
+      "order": 2,
+      "title":"my other dasboard"
+    }
+  ]
+}
+```
+
+## Delete a playlist
+
+`DELETE /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+DELETE /api/playlists/1 HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{}
+```
diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go
index a90b6425cb6..0963df7d4c4 100644
--- a/pkg/api/playlist.go
+++ b/pkg/api/playlist.go
@@ -160,6 +160,7 @@ func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
 
 func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
 	cmd.OrgId = c.OrgId
+	cmd.Id = c.ParamsInt64(":id")
 
 	if err := bus.Dispatch(&cmd); err != nil {
 		return Error(500, "Failed to save playlist", err)
diff --git a/pkg/models/playlist.go b/pkg/models/playlist.go
index 5c49bb9256c..c52da202293 100644
--- a/pkg/models/playlist.go
+++ b/pkg/models/playlist.go
@@ -63,7 +63,7 @@ type PlaylistDashboards []*PlaylistDashboard
 
 type UpdatePlaylistCommand struct {
 	OrgId    int64             `json:"-"`
-	Id       int64             `json:"id" binding:"Required"`
+	Id       int64             `json:"id"`
 	Name     string            `json:"name" binding:"Required"`
 	Interval string            `json:"interval"`
 	Items    []PlaylistItemDTO `json:"items"`

From 92d417f6b41a80cd6995a01fba99bb152363038a Mon Sep 17 00:00:00 2001
From: Jakob van Santen <jvansanten@gmail.com>
Date: Tue, 17 Jul 2018 20:10:12 +0200
Subject: [PATCH 034/380] Handle query string in storage public_url (#9351)
 (#12555)

---
 docs/sources/installation/configuration.md        |  2 +-
 pkg/components/imguploader/webdavuploader.go      | 15 ++++++++++++---
 pkg/components/imguploader/webdavuploader_test.go | 13 +++++++++++++
 3 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index f4fd6e49117..e3db7a1d60b 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -863,7 +863,7 @@ Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 Url to where Grafana will send PUT request with images
 
 ### public_url
-Optional parameter. Url to send to users in notifications, directly appended with the resulting uploaded file name.
+Optional parameter. Url to send to users in notifications. If the string contains the sequence ${file}, it will be replaced with the uploaded filename. Otherwise, the file name will be appended to the path part of the url, leaving any query string unchanged.
 
 ### username
 basic auth username
diff --git a/pkg/components/imguploader/webdavuploader.go b/pkg/components/imguploader/webdavuploader.go
index f5478ea8a2f..ed6b14725c0 100644
--- a/pkg/components/imguploader/webdavuploader.go
+++ b/pkg/components/imguploader/webdavuploader.go
@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"strings"
 	"time"
 
 	"github.com/grafana/grafana/pkg/util"
@@ -35,6 +36,16 @@ var netClient = &http.Client{
 	Transport: netTransport,
 }
 
+func (u *WebdavUploader) PublicURL(filename string) string {
+	if strings.Contains(u.public_url, "${file}") {
+		return strings.Replace(u.public_url, "${file}", filename, -1)
+	} else {
+		publicURL, _ := url.Parse(u.public_url)
+		publicURL.Path = path.Join(publicURL.Path, filename)
+		return publicURL.String()
+	}
+}
+
 func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
 	url, _ := url.Parse(u.url)
 	filename := util.GetRandomString(20) + ".png"
@@ -65,9 +76,7 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
 	}
 
 	if u.public_url != "" {
-		publicURL, _ := url.Parse(u.public_url)
-		publicURL.Path = path.Join(publicURL.Path, filename)
-		return publicURL.String(), nil
+		return u.PublicURL(filename), nil
 	}
 
 	return url.String(), nil
diff --git a/pkg/components/imguploader/webdavuploader_test.go b/pkg/components/imguploader/webdavuploader_test.go
index 5a8abd0542d..0178c9cda6c 100644
--- a/pkg/components/imguploader/webdavuploader_test.go
+++ b/pkg/components/imguploader/webdavuploader_test.go
@@ -2,6 +2,7 @@ package imguploader
 
 import (
 	"context"
+	"net/url"
 	"testing"
 
 	. "github.com/smartystreets/goconvey/convey"
@@ -26,3 +27,15 @@ func TestUploadToWebdav(t *testing.T) {
 		So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
 	})
 }
+
+func TestPublicURL(t *testing.T) {
+	Convey("Given a public URL with parameters, and no template", t, func() {
+		webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=")
+		parsed, _ := url.Parse(webdavUploader.PublicURL("fileyfile.png"))
+		So(parsed.Path, ShouldEndWith, "fileyfile.png")
+	})
+	Convey("Given a public URL with parameters, and a template", t, func() {
+		webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=${file}")
+		So(webdavUploader.PublicURL("fileyfile.png"), ShouldEndWith, "fileyfile.png")
+	})
+}

From f5cc7618c5816cd208d4e8b0f9442fabd0391a39 Mon Sep 17 00:00:00 2001
From: Daniel Lee <dan.limerick@gmail.com>
Date: Tue, 17 Jul 2018 22:17:00 +0200
Subject: [PATCH 035/380] alert: add missing test after refactor

---
 pkg/services/sqlstore/alert_test.go | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go
index 79fa99864e7..d97deb45f0e 100644
--- a/pkg/services/sqlstore/alert_test.go
+++ b/pkg/services/sqlstore/alert_test.go
@@ -13,7 +13,7 @@ func mockTimeNow() {
 	var timeSeed int64
 	timeNow = func() time.Time {
 		fakeNow := time.Unix(timeSeed, 0)
-		timeSeed += 1
+		timeSeed++
 		return fakeNow
 	}
 }
@@ -30,7 +30,7 @@ func TestAlertingDataAccess(t *testing.T) {
 		InitTestDB(t)
 
 		testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
-
+		evalData, _ := simplejson.NewJson([]byte(`{"test": "test"}`))
 		items := []*m.Alert{
 			{
 				PanelId:     1,
@@ -40,6 +40,7 @@ func TestAlertingDataAccess(t *testing.T) {
 				Message:     "Alerting message",
 				Settings:    simplejson.New(),
 				Frequency:   1,
+				EvalData:    evalData,
 			},
 		}
 
@@ -104,8 +105,18 @@ func TestAlertingDataAccess(t *testing.T) {
 
 			alert := alertQuery.Result[0]
 			So(err2, ShouldBeNil)
+			So(alert.Id, ShouldBeGreaterThan, 0)
+			So(alert.DashboardId, ShouldEqual, testDash.Id)
+			So(alert.PanelId, ShouldEqual, 1)
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.State, ShouldEqual, "pending")
+			So(alert.NewStateDate, ShouldNotBeNil)
+			So(alert.EvalData, ShouldNotBeNil)
+			So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
+			So(alert.EvalDate, ShouldNotBeNil)
+			So(alert.ExecutionError, ShouldEqual, "")
+			So(alert.DashboardUid, ShouldNotBeNil)
+			So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts")
 		})
 
 		Convey("Viewer cannot read alerts", func() {

From 3cb95fb40acbc445cf4bbdddeffb55fcfc5e5353 Mon Sep 17 00:00:00 2001
From: Daniel Lee <dan.limerick@gmail.com>
Date: Wed, 18 Jul 2018 12:05:45 +0200
Subject: [PATCH 036/380] pluginloader: expose flot gauge plugin

---
 public/app/features/plugins/plugin_loader.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts
index 641b5100703..cce494d0a60 100644
--- a/public/app/features/plugins/plugin_loader.ts
+++ b/public/app/features/plugins/plugin_loader.ts
@@ -126,6 +126,7 @@ import 'vendor/flot/jquery.flot.stackpercent';
 import 'vendor/flot/jquery.flot.fillbelow';
 import 'vendor/flot/jquery.flot.crosshair';
 import 'vendor/flot/jquery.flot.dashes';
+import 'vendor/flot/jquery.flot.gauge';
 
 const flotDeps = [
   'jquery.flot',
@@ -137,6 +138,7 @@ const flotDeps = [
   'jquery.flot.selection',
   'jquery.flot.stackpercent',
   'jquery.flot.events',
+  'jquery.flot.gauge',
 ];
 for (let flotDep of flotDeps) {
   exposeToPlugin(flotDep, { fakeDep: 1 });

From 3ab5ab36746194ac804123c69157df7561108635 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Wed, 18 Jul 2018 13:15:45 +0200
Subject: [PATCH 037/380] Fix label suggestions in Explore query field

- In 0425b477 the labels suggestions were refactored and a typo broke
  the look up for available lables of a metric
---
 public/app/containers/Explore/QueryField.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx
index bedb955b9b9..c9d71b3b49e 100644
--- a/public/app/containers/Explore/QueryField.tsx
+++ b/public/app/containers/Explore/QueryField.tsx
@@ -17,6 +17,7 @@ import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
 import Typeahead from './Typeahead';
 
 const EMPTY_METRIC = '';
+const METRIC_MARK = 'metric';
 export const TYPEAHEAD_DEBOUNCE = 300;
 
 function flattenSuggestions(s) {
@@ -135,7 +136,7 @@ class QueryField extends React.Component<any, any> {
     if (!this.state.metrics) {
       return;
     }
-    setPrismTokens(this.props.prismLanguage, 'metrics', this.state.metrics);
+    setPrismTokens(this.props.prismLanguage, METRIC_MARK, this.state.metrics);
 
     // Trigger re-render
     window.requestAnimationFrame(() => {
@@ -184,7 +185,7 @@ class QueryField extends React.Component<any, any> {
       let typeaheadContext = null;
 
       // Take first metric as lucky guess
-      const metricNode = editorNode.querySelector('.metric');
+      const metricNode = editorNode.querySelector(`.${METRIC_MARK}`);
 
       if (wrapperClasses.contains('context-range')) {
         // Rate ranges

From 913b8576f803c854ade10d424e111e6ba8c1e1bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Wed, 18 Jul 2018 13:20:28 +0200
Subject: [PATCH 038/380] docs: minor docs fix

---
 CHANGELOG.md                      | 1 +
 conf/ldap.toml                    | 2 +-
 docs/sources/installation/ldap.md | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ebb038546e..130fbbb7945 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
 * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
+* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
 
 ### Minor
 
diff --git a/conf/ldap.toml b/conf/ldap.toml
index 1b207d8424e..a74b2b6cc2c 100644
--- a/conf/ldap.toml
+++ b/conf/ldap.toml
@@ -72,7 +72,7 @@ email =  "email"
 [[servers.group_mappings]]
 group_dn = "cn=admins,dc=grafana,dc=org"
 org_role = "Admin"
-# To make user a instance admin  (Grafana Admin) uncomment line below
+# To make user an instance admin  (Grafana Admin) uncomment line below
 # grafana_admin = true
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used
 # org_id = 1
diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md
index ce77a1124e9..9a381b9e467 100644
--- a/docs/sources/installation/ldap.md
+++ b/docs/sources/installation/ldap.md
@@ -74,7 +74,7 @@ email =  "email"
 [[servers.group_mappings]]
 group_dn = "cn=admins,dc=grafana,dc=org"
 org_role = "Admin"
-# To make user a instance admin  (Grafana Admin) uncomment line below
+# To make user an instance admin  (Grafana Admin) uncomment line below
 # grafana_admin = true
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used.  Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
 # org_id = 1

From a4587cdeeeb86f153ff6b55d26209cda164633a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Wed, 18 Jul 2018 14:28:38 +0200
Subject: [PATCH 039/380] fix: datasource search was not working properly

---
 public/app/features/plugins/ds_list_ctrl.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/public/app/features/plugins/ds_list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts
index 577b931551a..89c760ae253 100644
--- a/public/app/features/plugins/ds_list_ctrl.ts
+++ b/public/app/features/plugins/ds_list_ctrl.ts
@@ -19,6 +19,7 @@ export class DataSourcesCtrl {
   onQueryUpdated() {
     let regex = new RegExp(this.searchQuery, 'ig');
     this.datasources = _.filter(this.unfiltered, item => {
+      regex.lastIndex = 0;
       return regex.test(item.name) || regex.test(item.type);
     });
   }

From ce64a3ccbcc94d1a7d1c70e90a6aa036531d2914 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Wed, 18 Jul 2018 15:40:30 +0200
Subject: [PATCH 040/380] added: replaces added to grafana

---
 build.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/build.go b/build.go
index 77cbde50c41..bcb9b2ddf7d 100644
--- a/build.go
+++ b/build.go
@@ -330,6 +330,7 @@ func createPackage(options linuxPackageOptions) {
 	name := "grafana"
 	if enterprise {
 		name += "-enterprise"
+		args = append(args, "--replaces", "grafana")
 	}
 	args = append(args, "--name", name)
 

From e413f026b99a125bef2cf64e6c561286bbd725f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Wed, 18 Jul 2018 07:21:32 -0700
Subject: [PATCH 041/380] fix: postgres/mysql engine cache was not being used,
 fixes #12636 (#12642)

---
 devenv/datasources.yaml | 4 +++-
 pkg/tsdb/sql_engine.go  | 1 +
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml
index 58368afdd27..241381097b1 100644
--- a/devenv/datasources.yaml
+++ b/devenv/datasources.yaml
@@ -63,7 +63,8 @@ datasources:
     url: localhost:5432
     database: grafana
     user: grafana
-    password: password
+    secureJsonData:
+      password: password
     jsonData:
       sslmode: "disable"
 
@@ -74,3 +75,4 @@ datasources:
       authType: credentials
       defaultRegion: eu-west-2
 
+
diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index 82a9b8f0d88..ec908aeb9de 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -68,6 +68,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
 	engine.SetMaxOpenConns(10)
 	engine.SetMaxIdleConns(10)
 
+	engineCache.versions[dsInfo.Id] = dsInfo.Version
 	engineCache.cache[dsInfo.Id] = engine
 	e.XormEngine = engine
 

From 0b421004ea3a41aa73fba414010ecbe5fa687f20 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Fri, 20 Jul 2018 09:59:04 +0200
Subject: [PATCH 042/380] built a component for delete button in tables,
 instead of using a modal to confirm it now does it in the row of the table,
 created a sass file for the component, the component uses css transitions for
 animation

---
 public/app/containers/Teams/TeamList.tsx      | 19 +----
 .../components/DeleteButton/DeleteButton.tsx  | 78 +++++++++++++++++++
 public/sass/_grafana.scss                     |  1 +
 public/sass/components/_delete_button.scss    | 49 ++++++++++++
 4 files changed, 131 insertions(+), 16 deletions(-)
 create mode 100644 public/app/core/components/DeleteButton/DeleteButton.tsx
 create mode 100644 public/sass/components/_delete_button.scss

diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx
index 4429764b1cc..475f8762c69 100644
--- a/public/app/containers/Teams/TeamList.tsx
+++ b/public/app/containers/Teams/TeamList.tsx
@@ -6,6 +6,7 @@ import { NavStore } from 'app/stores/NavStore/NavStore';
 import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore';
 import { BackendSrv } from 'app/core/services/backend_srv';
 import appEvents from 'app/core/app_events';
+import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
 
 interface Props {
   nav: typeof NavStore.Type;
@@ -28,18 +29,6 @@ export class TeamList extends React.Component<Props, any> {
   }
 
   deleteTeam(team: ITeam) {
-    appEvents.emit('confirm-modal', {
-      title: 'Delete',
-      text: 'Are you sure you want to delete Team ' + team.name + '?',
-      yesText: 'Delete',
-      icon: 'fa-warning',
-      onConfirm: () => {
-        this.deleteTeamConfirmed(team);
-      },
-    });
-  }
-
-  deleteTeamConfirmed(team) {
     this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this));
   }
 
@@ -67,9 +56,7 @@ export class TeamList extends React.Component<Props, any> {
           <a href={teamUrl}>{team.memberCount}</a>
         </td>
         <td className="text-right">
-          <a onClick={() => this.deleteTeam(team)} className="btn btn-danger btn-small">
-            <i className="fa fa-remove" />
-          </a>
+          <DeleteButton confirmDelete={() => this.deleteTeam(team)} />
         </td>
       </tr>
     );
@@ -102,7 +89,7 @@ export class TeamList extends React.Component<Props, any> {
             </a>
           </div>
 
-          <div className="admin-list-table">
+          <div className="admin-list-table tr-overflow">
             <table className="filter-table filter-table--hover form-inline">
               <thead>
                 <tr>
diff --git a/public/app/core/components/DeleteButton/DeleteButton.tsx b/public/app/core/components/DeleteButton/DeleteButton.tsx
new file mode 100644
index 00000000000..61a322b591e
--- /dev/null
+++ b/public/app/core/components/DeleteButton/DeleteButton.tsx
@@ -0,0 +1,78 @@
+import React, { Component } from 'react';
+
+export default class DeleteButton extends Component<any, any> {
+  state = {
+    deleteButton: 'delete-button--show',
+    confirmSpan: 'confirm-delete--removed',
+  };
+
+  handleDelete = event => {
+    if (event) {
+      event.preventDefault();
+    }
+
+    this.setState({
+      deleteButton: 'delete-button--hide',
+    });
+
+    setTimeout(() => {
+      this.setState({
+        deleteButton: 'delete-button--removed',
+      });
+    }, 100);
+
+    setTimeout(() => {
+      this.setState({
+        confirmSpan: 'confirm-delete--hide',
+      });
+    }, 100);
+
+    setTimeout(() => {
+      this.setState({
+        confirmSpan: 'confirm-delete--show',
+      });
+    }, 150);
+  };
+
+  cancelDelete = event => {
+    event.preventDefault();
+
+    this.setState({
+      confirmSpan: 'confirm-delete--hide',
+    });
+
+    setTimeout(() => {
+      this.setState({
+        confirmSpan: 'confirm-delete--removed',
+        deleteButton: 'delete-button--hide',
+      });
+    }, 140);
+
+    setTimeout(() => {
+      this.setState({
+        deleteButton: 'delete-button--show',
+      });
+    }, 190);
+  };
+
+  render() {
+    const { confirmDelete } = this.props;
+    return (
+      <span className="delete-button-container">
+        <a className={this.state.deleteButton + ' btn btn-danger btn-small'} onClick={this.handleDelete}>
+          <i className="fa fa-remove" />
+        </a>
+        <span className="confirm-delete-container">
+          <span className={this.state.confirmSpan}>
+            <a className="btn btn-small" onClick={this.cancelDelete}>
+              Cancel
+            </a>
+            <a className="btn btn-danger btn-small" onClick={confirmDelete}>
+              Confirm Delete
+            </a>
+          </span>
+        </span>
+      </span>
+    );
+  }
+}
diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss
index 9e3bec267ed..3a72bd45a1a 100644
--- a/public/sass/_grafana.scss
+++ b/public/sass/_grafana.scss
@@ -93,6 +93,7 @@
 @import 'components/form_select_box';
 @import 'components/user-picker';
 @import 'components/description-picker';
+@import 'components/delete_button';
 
 // PAGES
 @import 'pages/login';
diff --git a/public/sass/components/_delete_button.scss b/public/sass/components/_delete_button.scss
new file mode 100644
index 00000000000..19f32189d81
--- /dev/null
+++ b/public/sass/components/_delete_button.scss
@@ -0,0 +1,49 @@
+.delete-button-container {
+  max-width: 24px;
+  width: 24px;
+  direction: rtl;
+  max-height: 38px;
+  display: block;
+}
+
+.confirm-delete-container {
+  overflow: hidden;
+  width: 145px;
+  display: block;
+}
+
+.delete-button {
+  &--show {
+    display: inline-block;
+    opacity: 1;
+    transition: opacity 0.1s ease;
+  }
+
+  &--hide {
+    display: inline-block;
+    opacity: 0;
+    transition: opacity 0.1s ease;
+  }
+  &--removed {
+    display: none;
+  }
+}
+
+.confirm-delete {
+  &--show {
+    display: inline-block;
+    opacity: 1;
+    transition: opacity 0.08s ease-out, transform 0.1s ease-out;
+    transform: translateX(0);
+  }
+
+  &--hide {
+    display: inline-block;
+    opacity: 0;
+    transition: opacity 0.12s ease-in, transform 0.14s ease-in;
+    transform: translateX(100px);
+  }
+  &--removed {
+    display: none;
+  }
+}

From b8a4b7771ae72660fd16022920265750ce42e073 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Fri, 20 Jul 2018 11:09:24 +0200
Subject: [PATCH 043/380] removed import appEvents

---
 public/app/containers/Teams/TeamList.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx
index 475f8762c69..87d24f8ddd4 100644
--- a/public/app/containers/Teams/TeamList.tsx
+++ b/public/app/containers/Teams/TeamList.tsx
@@ -5,7 +5,6 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import { NavStore } from 'app/stores/NavStore/NavStore';
 import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore';
 import { BackendSrv } from 'app/core/services/backend_srv';
-import appEvents from 'app/core/app_events';
 import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
 
 interface Props {

From b6909eb3b00b10bfbb72eb4495cb89e0e7a625ca Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Fri, 20 Jul 2018 16:02:41 +0200
Subject: [PATCH 044/380] removed blue-dark variable with blue-light in
 light-theme, blue variable now has same value as blue-dark had before, should
 fix issue with any low contrast issues with blue in light-theme, this made
 query-blue variable unnecessery removed it, added variable for variable
 dropdown highlight background

---
 public/sass/_variables.dark.scss          |  4 +++-
 public/sass/_variables.light.scss         | 24 ++++++++++++-----------
 public/sass/components/_query_editor.scss |  6 +++---
 public/sass/components/_slate_editor.scss |  2 +-
 public/sass/components/_submenu.scss      |  2 +-
 public/sass/components/_timepicker.scss   |  2 +-
 6 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss
index eb73b014a93..01590ace585 100644
--- a/public/sass/_variables.dark.scss
+++ b/public/sass/_variables.dark.scss
@@ -44,7 +44,6 @@ $brand-success: $green;
 $brand-warning: $brand-primary;
 $brand-danger: $red;
 
-$query-blue: $blue;
 $query-red: $red;
 $query-green: $green;
 $query-purple: $purple;
@@ -347,3 +346,6 @@ $diff-json-changed-fg: $gray-5;
 $diff-json-changed-num: $text-color;
 
 $diff-json-icon: $gray-7;
+
+//Submenu
+$variable-option-bg: $blue-dark;
diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss
index 7e5e1b6a7f8..b6e9e7db979 100644
--- a/public/sass/_variables.light.scss
+++ b/public/sass/_variables.light.scss
@@ -30,8 +30,8 @@ $white: #fff;
 
 // Accent colors
 // -------------------------
-$blue: #61c2f2;
-$blue-dark: #0083b3;
+$blue: #0083b3;
+$blue-light: #00a8e6;
 $green: #3aa655;
 $red: #d44939;
 $yellow: #ff851b;
@@ -45,7 +45,6 @@ $brand-success: $green;
 $brand-warning: $orange;
 $brand-danger: $red;
 
-$query-blue: $blue-dark;
 $query-red: $red;
 $query-green: $green;
 $query-purple: $purple;
@@ -82,7 +81,7 @@ $page-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
 $link-color: $gray-1;
 $link-color-disabled: lighten($link-color, 30%);
 $link-hover-color: darken($link-color, 20%);
-$external-link-color: $blue;
+$external-link-color: $blue-light;
 
 // Typography
 // -------------------------
@@ -150,8 +149,8 @@ $scrollbarBorder: $gray-4;
 $btn-primary-bg: $brand-primary;
 $btn-primary-bg-hl: lighten($brand-primary, 8%);
 
-$btn-secondary-bg: $blue-dark;
-$btn-secondary-bg-hl: lighten($blue-dark, 4%);
+$btn-secondary-bg: $blue;
+$btn-secondary-bg-hl: lighten($blue, 4%);
 
 $btn-success-bg: lighten($green, 3%);
 $btn-success-bg-hl: darken($green, 3%);
@@ -168,7 +167,7 @@ $btn-inverse-text-color: $gray-1;
 $btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
 
 $btn-active-bg: $white;
-$btn-active-text-color: $blue-dark;
+$btn-active-text-color: $blue;
 
 $btn-link-color: $gray-1;
 
@@ -220,7 +219,7 @@ $search-filter-box-bg: $gray-7;
 // Typeahead
 $typeahead-shadow: 0 5px 10px 0 $gray-5;
 $typeahead-selected-bg: lighten($blue, 25%);
-$typeahead-selected-color: $blue-dark;
+$typeahead-selected-color: $blue;
 
 // Dropdowns
 // -------------------------
@@ -285,7 +284,7 @@ $info-text-color: $blue;
 $alert-error-bg: linear-gradient(90deg, #d44939, #e04d3d);
 $alert-success-bg: linear-gradient(90deg, #3aa655, #47b274);
 $alert-warning-bg: linear-gradient(90deg, #d44939, #e04d3d);
-$alert-info-bg: $blue-dark;
+$alert-info-bg: $blue;
 
 // popover
 $popover-bg: $page-bg;
@@ -293,7 +292,7 @@ $popover-color: $text-color;
 $popover-border-color: $gray-5;
 $popover-shadow: 0 0 20px $white;
 
-$popover-help-bg: $blue-dark;
+$popover-help-bg: $blue;
 $popover-help-color: $gray-6;
 $popover-error-bg: $btn-danger-bg;
 
@@ -310,7 +309,7 @@ $graph-tooltip-bg: $gray-5;
 $checkboxImageUrl: '../img/checkbox_white.png';
 
 // info box
-$info-box-background: linear-gradient(100deg, $blue-dark, darken($blue-dark, 5%));
+$info-box-background: linear-gradient(100deg, $blue, darken($blue, 5%));
 $info-box-color: $gray-7;
 
 // footer
@@ -356,3 +355,6 @@ $diff-json-new: #664e33;
 $diff-json-changed-fg: $gray-6;
 $diff-json-changed-num: $gray-4;
 $diff-json-icon: $gray-4;
+
+//Submenu
+$variable-option-bg: $blue-light;
diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss
index 6b2860d57bf..9fcfdf719ba 100644
--- a/public/sass/components/_query_editor.scss
+++ b/public/sass/components/_query_editor.scss
@@ -1,11 +1,11 @@
 .query-keyword {
   font-weight: $font-weight-semi-bold;
-  color: $query-blue;
+  color: $blue;
 }
 
 .gf-form-disabled {
   .query-keyword {
-    color: darken($query-blue, 20%);
+    color: darken($blue, 20%);
   }
 }
 
@@ -63,7 +63,7 @@
   }
   .gf-form-query-letter-cell-letter {
     font-weight: bold;
-    color: $query-blue;
+    color: $blue;
   }
   .gf-form-query-letter-cell-ds {
     color: $text-color-weak;
diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss
index de8a6e6d721..119c468292a 100644
--- a/public/sass/components/_slate_editor.scss
+++ b/public/sass/components/_slate_editor.scss
@@ -122,7 +122,7 @@
   .token.attr-value,
   .token.keyword,
   .token.class-name {
-    color: $query-blue;
+    color: $blue;
   }
 
   .token.regex,
diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss
index 0027e0b1999..1efd275bfad 100644
--- a/public/sass/components/_submenu.scss
+++ b/public/sass/components/_submenu.scss
@@ -138,7 +138,7 @@
 .variable-option {
   &:hover,
   &.highlighted {
-    background-color: $blue-dark;
+    background-color: $variable-option-bg;
   }
 }
 
diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss
index e4d8f4555e0..e12835d31c1 100644
--- a/public/sass/components/_timepicker.scss
+++ b/public/sass/components/_timepicker.scss
@@ -77,7 +77,7 @@
     border: none;
     color: $text-color;
     &.active span {
-      color: $query-blue;
+      color: $blue;
       font-weight: bold;
     }
     .text-info {

From 3297ae462da26f004c7d8bf4ef1da552aef84432 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 20 Jul 2018 17:07:17 +0200
Subject: [PATCH 045/380] Datasource for Grafana logging platform

- new builtin datasource plugin "Logging" (likely going to be renamed)
- plugin implements no panel ctrls yet, only ships datasource
- new models for logging data as first class citizen (aside from table
  and time_series model)
- Logs as new view for Explore
- JSON view for development

Testable only against existing logish deployment.
Then test with queries like `{job="..."} regexp`.
---
 pkg/plugins/datasource_plugin.go              |   2 +
 public/app/containers/Explore/Explore.tsx     | 108 +++++++++++---
 public/app/containers/Explore/JSONViewer.tsx  |   9 ++
 public/app/containers/Explore/Logs.tsx        |  66 +++++++++
 public/app/containers/Explore/QueryField.tsx  |   1 +
 public/app/core/logs_model.ts                 |  29 ++++
 .../app/features/plugins/built_in_plugins.ts  |   2 +
 .../app/plugins/datasource/logging/README.md  |   3 +
 .../datasource/logging/datasource.jest.ts     |  38 +++++
 .../plugins/datasource/logging/datasource.ts  | 134 ++++++++++++++++++
 .../datasource/logging/img/grafana_icon.svg   |  57 ++++++++
 .../app/plugins/datasource/logging/module.ts  |   7 +
 .../datasource/logging/partials/config.html   |   2 +
 .../plugins/datasource/logging/plugin.json    |  28 ++++
 .../logging/result_transformer.jest.ts        |  45 ++++++
 .../datasource/logging/result_transformer.ts  |  71 ++++++++++
 public/sass/pages/_explore.scss               |  37 +++++
 17 files changed, 620 insertions(+), 19 deletions(-)
 create mode 100644 public/app/containers/Explore/JSONViewer.tsx
 create mode 100644 public/app/containers/Explore/Logs.tsx
 create mode 100644 public/app/core/logs_model.ts
 create mode 100644 public/app/plugins/datasource/logging/README.md
 create mode 100644 public/app/plugins/datasource/logging/datasource.jest.ts
 create mode 100644 public/app/plugins/datasource/logging/datasource.ts
 create mode 100644 public/app/plugins/datasource/logging/img/grafana_icon.svg
 create mode 100644 public/app/plugins/datasource/logging/module.ts
 create mode 100644 public/app/plugins/datasource/logging/partials/config.html
 create mode 100644 public/app/plugins/datasource/logging/plugin.json
 create mode 100644 public/app/plugins/datasource/logging/result_transformer.jest.ts
 create mode 100644 public/app/plugins/datasource/logging/result_transformer.ts

diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go
index cef35a2e7d9..ff44805e35f 100644
--- a/pkg/plugins/datasource_plugin.go
+++ b/pkg/plugins/datasource_plugin.go
@@ -17,12 +17,14 @@ import (
 	plugin "github.com/hashicorp/go-plugin"
 )
 
+// DataSourcePlugin contains all metadata about a datasource plugin
 type DataSourcePlugin struct {
 	FrontendPluginBase
 	Annotations  bool              `json:"annotations"`
 	Metrics      bool              `json:"metrics"`
 	Alerting     bool              `json:"alerting"`
 	Explore      bool              `json:"explore"`
+	Logs         bool              `json:"logs"`
 	QueryOptions map[string]bool   `json:"queryOptions,omitempty"`
 	BuiltIn      bool              `json:"builtIn,omitempty"`
 	Mixed        bool              `json:"mixed,omitempty"`
diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index e50c06c8b17..178e53198d4 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -11,6 +11,7 @@ import { parse as parseDate } from 'app/core/utils/datemath';
 import ElapsedTime from './ElapsedTime';
 import QueryRows from './QueryRows';
 import Graph from './Graph';
+import Logs from './Logs';
 import Table from './Table';
 import TimePicker, { DEFAULT_RANGE } from './TimePicker';
 import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
@@ -58,12 +59,17 @@ interface IExploreState {
   initialDatasource?: string;
   latency: number;
   loading: any;
+  logsResult: any;
   queries: any;
   queryError: any;
   range: any;
   requestOptions: any;
   showingGraph: boolean;
+  showingLogs: boolean;
   showingTable: boolean;
+  supportsGraph: boolean | null;
+  supportsLogs: boolean | null;
+  supportsTable: boolean | null;
   tableResult: any;
 }
 
@@ -82,12 +88,17 @@ export class Explore extends React.Component<any, IExploreState> {
       initialDatasource: datasource,
       latency: 0,
       loading: false,
+      logsResult: null,
       queries: ensureQueries(queries),
       queryError: null,
       range: range || { ...DEFAULT_RANGE },
       requestOptions: null,
       showingGraph: true,
+      showingLogs: true,
       showingTable: true,
+      supportsGraph: null,
+      supportsLogs: null,
+      supportsTable: null,
       tableResult: null,
       ...props.initialState,
     };
@@ -124,17 +135,29 @@ export class Explore extends React.Component<any, IExploreState> {
   }
 
   async setDatasource(datasource) {
+    const supportsGraph = datasource.meta.metrics;
+    const supportsLogs = datasource.meta.logs;
+    const supportsTable = datasource.meta.metrics;
+    let datasourceError = null;
+
     try {
       const testResult = await datasource.testDatasource();
-      if (testResult.status === 'success') {
-        this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
-      } else {
-        this.setState({ datasource: datasource, datasourceError: testResult.message, datasourceLoading: false });
-      }
+      datasourceError = testResult.status === 'success' ? null : testResult.message;
     } catch (error) {
-      const message = (error && error.statusText) || error;
-      this.setState({ datasource: datasource, datasourceError: message, datasourceLoading: false });
+      datasourceError = (error && error.statusText) || error;
     }
+
+    this.setState(
+      {
+        datasource,
+        datasourceError,
+        supportsGraph,
+        supportsLogs,
+        supportsTable,
+        datasourceLoading: false,
+      },
+      () => datasourceError === null && this.handleSubmit()
+    );
   }
 
   getRef = el => {
@@ -157,6 +180,7 @@ export class Explore extends React.Component<any, IExploreState> {
       datasourceError: null,
       datasourceLoading: true,
       graphResult: null,
+      logsResult: null,
       tableResult: null,
     });
     const datasource = await this.props.datasourceSrv.get(option.value);
@@ -193,6 +217,10 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState(state => ({ showingGraph: !state.showingGraph }));
   };
 
+  handleClickLogsButton = () => {
+    this.setState(state => ({ showingLogs: !state.showingLogs }));
+  };
+
   handleClickSplit = () => {
     const { onChangeSplit } = this.props;
     if (onChangeSplit) {
@@ -214,16 +242,19 @@ export class Explore extends React.Component<any, IExploreState> {
   };
 
   handleSubmit = () => {
-    const { showingGraph, showingTable } = this.state;
-    if (showingTable) {
+    const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
+    if (showingTable && supportsTable) {
       this.runTableQuery();
     }
-    if (showingGraph) {
+    if (showingGraph && supportsGraph) {
       this.runGraphQuery();
     }
+    if (showingLogs && supportsLogs) {
+      this.runLogsQuery();
+    }
   };
 
-  buildQueryOptions(targetOptions: { format: string; instant: boolean }) {
+  buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
     const { datasource, queries, range } = this.state;
     const resolution = this.el.offsetWidth;
     const absoluteRange = {
@@ -285,6 +316,29 @@ export class Explore extends React.Component<any, IExploreState> {
     }
   }
 
+  async runLogsQuery() {
+    const { datasource, queries } = this.state;
+    if (!hasQuery(queries)) {
+      return;
+    }
+    this.setState({ latency: 0, loading: true, queryError: null, logsResult: null });
+    const now = Date.now();
+    const options = this.buildQueryOptions({
+      format: 'logs',
+    });
+
+    try {
+      const res = await datasource.query(options);
+      const logsData = res.data;
+      const latency = Date.now() - now;
+      this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
+    } catch (response) {
+      console.error(response);
+      const queryError = response.data ? response.data.error : response;
+      this.setState({ loading: false, queryError });
+    }
+  }
+
   request = url => {
     const { datasource } = this.state;
     return datasource.metadataRequest(url);
@@ -300,17 +354,23 @@ export class Explore extends React.Component<any, IExploreState> {
       graphResult,
       latency,
       loading,
+      logsResult,
       queries,
       queryError,
       range,
       requestOptions,
       showingGraph,
+      showingLogs,
       showingTable,
+      supportsGraph,
+      supportsLogs,
+      supportsTable,
       tableResult,
     } = this.state;
     const showingBoth = showingGraph && showingTable;
     const graphHeight = showingBoth ? '200px' : '400px';
     const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
+    const logsButtonActive = showingLogs ? 'active' : '';
     const tableButtonActive = showingBoth || showingTable ? 'active' : '';
     const exploreClass = split ? 'explore explore-split' : 'explore';
     const datasources = datasourceSrv.getExploreSources().map(ds => ({
@@ -357,12 +417,21 @@ export class Explore extends React.Component<any, IExploreState> {
             </div>
           ) : null}
           <div className="navbar-buttons">
-            <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
-              Graph
-            </button>
-            <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
-              Table
-            </button>
+            {supportsGraph ? (
+              <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
+                Graph
+              </button>
+            ) : null}
+            {supportsTable ? (
+              <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
+                Table
+              </button>
+            ) : null}
+            {supportsLogs ? (
+              <button className={`btn navbar-button ${logsButtonActive}`} onClick={this.handleClickLogsButton}>
+                Logs
+              </button>
+            ) : null}
           </div>
           <TimePicker range={range} onChangeTime={this.handleChangeTime} />
           <div className="navbar-buttons relative">
@@ -395,7 +464,7 @@ export class Explore extends React.Component<any, IExploreState> {
             />
             {queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
             <main className="m-t-2">
-              {showingGraph ? (
+              {supportsGraph && showingGraph ? (
                 <Graph
                   data={graphResult}
                   id={`explore-graph-${position}`}
@@ -404,7 +473,8 @@ export class Explore extends React.Component<any, IExploreState> {
                   split={split}
                 />
               ) : null}
-              {showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
+              {supportsTable && showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
+              {supportsLogs && showingLogs ? <Logs data={logsResult} /> : null}
             </main>
           </div>
         ) : null}
diff --git a/public/app/containers/Explore/JSONViewer.tsx b/public/app/containers/Explore/JSONViewer.tsx
new file mode 100644
index 00000000000..d0dbad78169
--- /dev/null
+++ b/public/app/containers/Explore/JSONViewer.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+export default function({ value }) {
+  return (
+    <div>
+      <pre>{JSON.stringify(value, undefined, 2)}</pre>
+    </div>
+  );
+}
diff --git a/public/app/containers/Explore/Logs.tsx b/public/app/containers/Explore/Logs.tsx
new file mode 100644
index 00000000000..10d7827a9a3
--- /dev/null
+++ b/public/app/containers/Explore/Logs.tsx
@@ -0,0 +1,66 @@
+import React, { Fragment, PureComponent } from 'react';
+
+import { LogsModel, LogRow } from 'app/core/logs_model';
+
+interface LogsProps {
+  className?: string;
+  data: LogsModel;
+}
+
+const EXAMPLE_QUERY = '{job="default/prometheus"}';
+
+const Entry: React.SFC<LogRow> = props => {
+  const { entry, searchMatches } = props;
+  if (searchMatches && searchMatches.length > 0) {
+    let lastMatchEnd = 0;
+    const spans = searchMatches.reduce((acc, match, i) => {
+      // Insert non-match
+      if (match.start !== lastMatchEnd) {
+        acc.push(<>{entry.slice(lastMatchEnd, match.start)}</>);
+      }
+      // Match
+      acc.push(
+        <span className="logs-row-match-highlight" title={`Matching expression: ${match.text}`}>
+          {entry.substr(match.start, match.length)}
+        </span>
+      );
+      lastMatchEnd = match.start + match.length;
+      // Non-matching end
+      if (i === searchMatches.length - 1) {
+        acc.push(<>{entry.slice(lastMatchEnd)}</>);
+      }
+      return acc;
+    }, []);
+    return <>{spans}</>;
+  }
+  return <>{props.entry}</>;
+};
+
+export default class Logs extends PureComponent<LogsProps, any> {
+  render() {
+    const { className = '', data } = this.props;
+    const hasData = data && data.rows && data.rows.length > 0;
+    return (
+      <div className={`${className} logs`}>
+        {hasData ? (
+          <div className="logs-entries panel-container">
+            {data.rows.map(row => (
+              <Fragment key={row.key}>
+                <div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
+                <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
+                <div>
+                  <Entry {...row} />
+                </div>
+              </Fragment>
+            ))}
+          </div>
+        ) : null}
+        {!hasData ? (
+          <div className="panel-container">
+            Enter a query like <code>{EXAMPLE_QUERY}</code>
+          </div>
+        ) : null}
+      </div>
+    );
+  }
+}
diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx
index c9d71b3b49e..41f6d53541c 100644
--- a/public/app/containers/Explore/QueryField.tsx
+++ b/public/app/containers/Explore/QueryField.tsx
@@ -417,6 +417,7 @@ class QueryField extends React.Component<any, any> {
     const url = `/api/v1/label/${key}/values`;
     try {
       const res = await this.request(url);
+      console.log(res);
       const body = await (res.data || res.json());
       const pairs = this.state.labelValues[EMPTY_METRIC];
       const values = {
diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts
new file mode 100644
index 00000000000..46e95a471ce
--- /dev/null
+++ b/public/app/core/logs_model.ts
@@ -0,0 +1,29 @@
+export enum LogLevel {
+  crit = 'crit',
+  warn = 'warn',
+  err = 'error',
+  error = 'error',
+  info = 'info',
+  debug = 'debug',
+  trace = 'trace',
+}
+
+export interface LogSearchMatch {
+  start: number;
+  length: number;
+  text?: string;
+}
+
+export interface LogRow {
+  key: string;
+  entry: string;
+  logLevel: LogLevel;
+  timestamp: string;
+  timeFromNow: string;
+  timeLocal: string;
+  searchMatches?: LogSearchMatch[];
+}
+
+export interface LogsModel {
+  rows: LogRow[];
+}
diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts
index 656ce2bfa38..2c5bf459eda 100644
--- a/public/app/features/plugins/built_in_plugins.ts
+++ b/public/app/features/plugins/built_in_plugins.ts
@@ -4,6 +4,7 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul
 import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
 import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
 import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
+import * as loggingPlugin from 'app/plugins/datasource/logging/module';
 import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
 import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
 import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
@@ -28,6 +29,7 @@ const builtInPlugins = {
   'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
   'app/plugins/datasource/grafana/module': grafanaPlugin,
   'app/plugins/datasource/influxdb/module': influxdbPlugin,
+  'app/plugins/datasource/logging/module': loggingPlugin,
   'app/plugins/datasource/mixed/module': mixedPlugin,
   'app/plugins/datasource/mysql/module': mysqlPlugin,
   'app/plugins/datasource/postgres/module': postgresPlugin,
diff --git a/public/app/plugins/datasource/logging/README.md b/public/app/plugins/datasource/logging/README.md
new file mode 100644
index 00000000000..33372605973
--- /dev/null
+++ b/public/app/plugins/datasource/logging/README.md
@@ -0,0 +1,3 @@
+# Grafana Logging Datasource -  Native Plugin
+
+This is a **built in** datasource that allows you to connect to Grafana's logging service.
\ No newline at end of file
diff --git a/public/app/plugins/datasource/logging/datasource.jest.ts b/public/app/plugins/datasource/logging/datasource.jest.ts
new file mode 100644
index 00000000000..212d352dfca
--- /dev/null
+++ b/public/app/plugins/datasource/logging/datasource.jest.ts
@@ -0,0 +1,38 @@
+import { parseQuery } from './datasource';
+
+describe('parseQuery', () => {
+  it('returns empty for empty string', () => {
+    expect(parseQuery('')).toEqual({
+      query: '',
+      regexp: '',
+    });
+  });
+
+  it('returns regexp for strings without query', () => {
+    expect(parseQuery('test')).toEqual({
+      query: '',
+      regexp: 'test',
+    });
+  });
+
+  it('returns query for strings without regexp', () => {
+    expect(parseQuery('{foo="bar"}')).toEqual({
+      query: '{foo="bar"}',
+      regexp: '',
+    });
+  });
+
+  it('returns query for strings with query and search string', () => {
+    expect(parseQuery('x {foo="bar"}')).toEqual({
+      query: '{foo="bar"}',
+      regexp: 'x',
+    });
+  });
+
+  it('returns query for strings with query and regexp', () => {
+    expect(parseQuery('{foo="bar"} x|y')).toEqual({
+      query: '{foo="bar"}',
+      regexp: 'x|y',
+    });
+  });
+});
diff --git a/public/app/plugins/datasource/logging/datasource.ts b/public/app/plugins/datasource/logging/datasource.ts
new file mode 100644
index 00000000000..22edba5807a
--- /dev/null
+++ b/public/app/plugins/datasource/logging/datasource.ts
@@ -0,0 +1,134 @@
+import _ from 'lodash';
+
+import * as dateMath from 'app/core/utils/datemath';
+
+import { processStreams } from './result_transformer';
+
+const DEFAULT_LIMIT = 100;
+
+const DEFAULT_QUERY_PARAMS = {
+  direction: 'BACKWARD',
+  limit: DEFAULT_LIMIT,
+  regexp: '',
+  query: '',
+};
+
+const QUERY_REGEXP = /({\w+="[^"]+"})?\s*(\w[^{]+)?\s*({\w+="[^"]+"})?/;
+export function parseQuery(input: string) {
+  const match = input.match(QUERY_REGEXP);
+  let query = '';
+  let regexp = '';
+
+  if (match) {
+    if (match[1]) {
+      query = match[1];
+    }
+    if (match[2]) {
+      regexp = match[2].trim();
+    }
+    if (match[3]) {
+      if (match[1]) {
+        query = `${match[1].slice(0, -1)},${match[3].slice(1)}`;
+      } else {
+        query = match[3];
+      }
+    }
+  }
+
+  return { query, regexp };
+}
+
+function serializeParams(data: any) {
+  return Object.keys(data)
+    .map(k => {
+      const v = data[k];
+      return encodeURIComponent(k) + '=' + encodeURIComponent(v);
+    })
+    .join('&');
+}
+
+export default class LoggingDatasource {
+  /** @ngInject */
+  constructor(private instanceSettings, private backendSrv, private templateSrv) {}
+
+  _request(apiUrl: string, data?, options?: any) {
+    const baseUrl = this.instanceSettings.url;
+    const params = data ? serializeParams(data) : '';
+    const url = `${baseUrl}${apiUrl}?${params}`;
+    const req = {
+      ...options,
+      url,
+    };
+    return this.backendSrv.datasourceRequest(req);
+  }
+
+  prepareQueryTarget(target, options) {
+    const interpolated = this.templateSrv.replace(target.expr);
+    const start = this.getTime(options.range.from, false);
+    const end = this.getTime(options.range.to, true);
+    return {
+      ...DEFAULT_QUERY_PARAMS,
+      ...parseQuery(interpolated),
+      start,
+      end,
+    };
+  }
+
+  query(options) {
+    const queryTargets = options.targets
+      .filter(target => target.expr)
+      .map(target => this.prepareQueryTarget(target, options));
+    if (queryTargets.length === 0) {
+      return Promise.resolve({ data: [] });
+    }
+
+    const queries = queryTargets.map(target => this._request('/api/prom/query', target));
+
+    return Promise.all(queries).then((results: any[]) => {
+      // Flatten streams from multiple queries
+      const allStreams = results.reduce((acc, response, i) => {
+        const streams = response.data.streams || [];
+        // Inject search for match highlighting
+        const search = queryTargets[i].regexp;
+        streams.forEach(s => {
+          s.search = search;
+        });
+        return [...acc, ...streams];
+      }, []);
+      const model = processStreams(allStreams, DEFAULT_LIMIT);
+      return { data: model };
+    });
+  }
+
+  metadataRequest(url) {
+    // HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField
+    const apiUrl = url.replace('v1', 'prom');
+    return this._request(apiUrl, { silent: true }).then(res => {
+      const data = { data: { data: res.data.values || [] } };
+      return data;
+    });
+  }
+
+  getTime(date, roundUp) {
+    if (_.isString(date)) {
+      date = dateMath.parse(date, roundUp);
+    }
+    return Math.ceil(date.valueOf() * 1e6);
+  }
+
+  testDatasource() {
+    return this._request('/api/prom/label')
+      .then(res => {
+        if (res && res.data && res.data.values && res.data.values.length > 0) {
+          return { status: 'success', message: 'Data source connected and labels found.' };
+        }
+        return {
+          status: 'error',
+          message: 'Data source connected, but no labels received. Verify that logging is configured properly.',
+        };
+      })
+      .catch(err => {
+        return { status: 'error', message: err.message };
+      });
+  }
+}
diff --git a/public/app/plugins/datasource/logging/img/grafana_icon.svg b/public/app/plugins/datasource/logging/img/grafana_icon.svg
new file mode 100644
index 00000000000..72702223dc7
--- /dev/null
+++ b/public/app/plugins/datasource/logging/img/grafana_icon.svg
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:url(#SVGID_1_);}
+</style>
+<g id="Layer_1_1_">
+</g>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="445.4948" x2="175.5" y2="114.0346">
+	<stop  offset="0" style="stop-color:#FFF100"/>
+	<stop  offset="1" style="stop-color:#F05A28"/>
+</linearGradient>
+<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
+	c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
+	c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
+	c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
+	c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
+	c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
+	C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
+	c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
+	c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
+	c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
+	c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
+	c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
+	l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
+	c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
+	c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
+	c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
+	c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
+	c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
+	c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
+	c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
+	c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
+	c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
+	c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
+	c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
+	c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
+	c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
+	c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
+	l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
+	c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
+	c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
+	c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
+	c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
+	l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
+	c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
+	c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
+	c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
+	c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
+	c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
+	l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
+	c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
+	c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
+	c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
+	c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
+</svg>
diff --git a/public/app/plugins/datasource/logging/module.ts b/public/app/plugins/datasource/logging/module.ts
new file mode 100644
index 00000000000..5e3ffb3282a
--- /dev/null
+++ b/public/app/plugins/datasource/logging/module.ts
@@ -0,0 +1,7 @@
+import Datasource from './datasource';
+
+export class LoggingConfigCtrl {
+  static templateUrl = 'partials/config.html';
+}
+
+export { Datasource, LoggingConfigCtrl as ConfigCtrl };
diff --git a/public/app/plugins/datasource/logging/partials/config.html b/public/app/plugins/datasource/logging/partials/config.html
new file mode 100644
index 00000000000..8e79cc0adcc
--- /dev/null
+++ b/public/app/plugins/datasource/logging/partials/config.html
@@ -0,0 +1,2 @@
+<datasource-http-settings current="ctrl.current" no-direct-access="true">
+</datasource-http-settings>
\ No newline at end of file
diff --git a/public/app/plugins/datasource/logging/plugin.json b/public/app/plugins/datasource/logging/plugin.json
new file mode 100644
index 00000000000..9aa844f21cb
--- /dev/null
+++ b/public/app/plugins/datasource/logging/plugin.json
@@ -0,0 +1,28 @@
+{
+  "type": "datasource",
+  "name": "Grafana Logging",
+  "id": "logging",
+  "metrics": false,
+  "alerting": false,
+  "annotations": false,
+  "logs": true,
+  "explore": true,
+  "info": {
+    "description": "Grafana Logging Data Source for Grafana",
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/grafana_icon.svg",
+      "large": "img/grafana_icon.svg"
+    },
+    "links": [
+      {
+        "name": "Grafana Logging",
+        "url": "https://grafana.com/"
+      }
+    ],
+    "version": "5.3.0"
+  }
+}
\ No newline at end of file
diff --git a/public/app/plugins/datasource/logging/result_transformer.jest.ts b/public/app/plugins/datasource/logging/result_transformer.jest.ts
new file mode 100644
index 00000000000..0d203f748ba
--- /dev/null
+++ b/public/app/plugins/datasource/logging/result_transformer.jest.ts
@@ -0,0 +1,45 @@
+import { LogLevel } from 'app/core/logs_model';
+
+import { getLogLevel, getSearchMatches } from './result_transformer';
+
+describe('getSearchMatches()', () => {
+  it('gets no matches for when search and or line are empty', () => {
+    expect(getSearchMatches('', '')).toEqual([]);
+    expect(getSearchMatches('foo', '')).toEqual([]);
+    expect(getSearchMatches('', 'foo')).toEqual([]);
+  });
+
+  it('gets no matches for unmatched search string', () => {
+    expect(getSearchMatches('foo', 'bar')).toEqual([]);
+  });
+
+  it('gets matches for matched search string', () => {
+    expect(getSearchMatches('foo', 'foo')).toEqual([{ length: 3, start: 0, text: 'foo' }]);
+    expect(getSearchMatches(' foo ', 'foo')).toEqual([{ length: 3, start: 1, text: 'foo' }]);
+  });
+
+  expect(getSearchMatches(' foo foo bar ', 'foo|bar')).toEqual([
+    { length: 3, start: 1, text: 'foo' },
+    { length: 3, start: 5, text: 'foo' },
+    { length: 3, start: 9, text: 'bar' },
+  ]);
+});
+
+describe('getLoglevel()', () => {
+  it('returns no log level on empty line', () => {
+    expect(getLogLevel('')).toBe(undefined);
+  });
+
+  it('returns no log level on when level is part of a word', () => {
+    expect(getLogLevel('this is a warning')).toBe(undefined);
+  });
+
+  it('returns log level on line contains a log level', () => {
+    expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
+    expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
+  });
+
+  it('returns first log level found', () => {
+    expect(getLogLevel('WARN this could be a debug message')).toBe(LogLevel.warn);
+  });
+});
diff --git a/public/app/plugins/datasource/logging/result_transformer.ts b/public/app/plugins/datasource/logging/result_transformer.ts
new file mode 100644
index 00000000000..e238778614c
--- /dev/null
+++ b/public/app/plugins/datasource/logging/result_transformer.ts
@@ -0,0 +1,71 @@
+import _ from 'lodash';
+import moment from 'moment';
+
+import { LogLevel, LogsModel, LogRow } from 'app/core/logs_model';
+
+export function getLogLevel(line: string): LogLevel {
+  if (!line) {
+    return undefined;
+  }
+  let level: LogLevel;
+  Object.keys(LogLevel).forEach(key => {
+    if (!level) {
+      const regexp = new RegExp(`\\b${key}\\b`, 'i');
+      if (regexp.test(line)) {
+        level = LogLevel[key];
+      }
+    }
+  });
+  return level;
+}
+
+export function getSearchMatches(line: string, search: string) {
+  // Empty search can send re.exec() into infinite loop, exit early
+  if (!line || !search) {
+    return [];
+  }
+  const regexp = new RegExp(`(?:${search})`, 'g');
+  const matches = [];
+  let match;
+  while ((match = regexp.exec(line))) {
+    matches.push({
+      text: match[0],
+      start: match.index,
+      length: match[0].length,
+    });
+  }
+  return matches;
+}
+
+export function processEntry(entry: { line: string; timestamp: string }, stream): LogRow {
+  const { line, timestamp } = entry;
+  const { labels } = stream;
+  const key = `EK${timestamp}${labels}`;
+  const time = moment(timestamp);
+  const timeFromNow = time.fromNow();
+  const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
+  const searchMatches = getSearchMatches(line, stream.search);
+  const logLevel = getLogLevel(line);
+
+  return {
+    key,
+    logLevel,
+    searchMatches,
+    timeFromNow,
+    timeLocal,
+    entry: line,
+    timestamp: timestamp,
+  };
+}
+
+export function processStreams(streams, limit?: number): LogsModel {
+  const combinedEntries = streams.reduce((acc, stream) => {
+    return [...acc, ...stream.entries.map(entry => processEntry(entry, stream))];
+  }, []);
+  const sortedEntries = _.chain(combinedEntries)
+    .sortBy('timestamp')
+    .reverse()
+    .slice(0, limit || combinedEntries.length)
+    .value();
+  return { rows: sortedEntries };
+}
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index e1b170c636d..158f0eb68ad 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -97,3 +97,40 @@
 .query-row-tools {
   width: 4rem;
 }
+
+.explore {
+  .logs {
+    .logs-entries {
+      display: grid;
+      grid-column-gap: 1rem;
+      grid-row-gap: 0.1rem;
+      grid-template-columns: 4px minmax(100px, max-content) 1fr;
+      font-family: $font-family-monospace;
+    }
+
+    .logs-row-match-highlight {
+      background-color: lighten($blue, 20%);
+    }
+
+    .logs-row-level {
+      background-color: transparent;
+      margin: 6px 0;
+      border-radius: 2px;
+      opacity: 0.8;
+    }
+
+    .logs-row-level-crit,
+    .logs-row-level-error,
+    .logs-row-level-err {
+      background-color: $red;
+    }
+
+    .logs-row-level-warn {
+      background-color: $orange;
+    }
+
+    .logs-row-level-info {
+      background-color: $green;
+    }
+  }
+}

From a2574ac068e0d6adec9727901784d5ac1cfbc749 Mon Sep 17 00:00:00 2001
From: Kim Christensen <kimworking@gmail.com>
Date: Fri, 13 Jul 2018 13:24:56 +0200
Subject: [PATCH 046/380] Support timeFilter in templating for InfluxDB

After support for queries in template variables was added to InfluxDB,
it can be necessary to added dymanic time constraints. This can now be
done changing the variable refresh to "On Time Range Changed" for
InfluxDB
---
 public/app/plugins/datasource/influxdb/datasource.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts
index f971ac2f649..b9f2b2e03fb 100644
--- a/public/app/plugins/datasource/influxdb/datasource.ts
+++ b/public/app/plugins/datasource/influxdb/datasource.ts
@@ -187,6 +187,11 @@ export default class InfluxDatasource {
       return this.$q.when({ results: [] });
     }
 
+    if (options && options.range) {
+      var timeFilter = this.getTimeFilter({ rangeRaw: options.range });
+      query = query.replace('$timeFilter', timeFilter);
+    }
+
     return this._influxRequest('GET', '/query', { q: query, epoch: 'ms' }, options);
   }
 

From dd81f4381de8e663c17e12595b33b46020c153cf Mon Sep 17 00:00:00 2001
From: Kim Christensen <kimworking@gmail.com>
Date: Sat, 21 Jul 2018 02:13:41 +0200
Subject: [PATCH 047/380] Add unit test for InfluxDB datasource

---
 .../influxdb/specs/datasource.jest.ts         | 53 +++++++++++++++++++
 1 file changed, 53 insertions(+)
 create mode 100644 public/app/plugins/datasource/influxdb/specs/datasource.jest.ts

diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts b/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts
new file mode 100644
index 00000000000..6ccbf843dd5
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts
@@ -0,0 +1,53 @@
+import InfluxDatasource from '../datasource';
+import $q from 'q';
+import { TemplateSrvStub } from 'test/specs/helpers';
+
+describe('InfluxDataSource', () => {
+  let ctx: any = {
+    backendSrv: {},
+    $q: $q,
+    templateSrv: new TemplateSrvStub(),
+    instanceSettings: { url: 'url', name: 'influxDb', jsonData: {} },
+  };
+
+  beforeEach(function() {
+    ctx.instanceSettings.url = '/api/datasources/proxy/1';
+    ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv);
+  });
+
+  describe('When issuing metricFindQuery', () => {
+    let query = 'SELECT max(value) FROM measurement WHERE $timeFilter';
+    let queryOptions: any = {
+      range: {
+        from: '2018-01-01 00:00:00',
+        to: '2018-01-02 00:00:00',
+      },
+    };
+    let requestQuery;
+
+    beforeEach(async () => {
+      ctx.backendSrv.datasourceRequest = function(req) {
+        requestQuery = req.params.q;
+        return ctx.$q.when({
+          results: [
+            {
+              series: [
+                {
+                  name: 'measurement',
+                  columns: ['max'],
+                  values: [[1]],
+                },
+              ],
+            },
+          ],
+        });
+      };
+
+      await ctx.ds.metricFindQuery(query, queryOptions).then(function(_) {});
+    });
+
+    it('should replace $timefilter', () => {
+      expect(requestQuery).toMatch('time >= 1514761200000ms and time <= 1514847600000ms');
+    });
+  });
+});

From 26aa575cb4208a0fa36fde074bc3f26eb9d3f56e Mon Sep 17 00:00:00 2001
From: yogyrahmawan <yogy.frestarahmawan@gmail.com>
Date: Sun, 22 Jul 2018 08:04:57 +0700
Subject: [PATCH 048/380] escaping ssl mode on postgres param

---
 pkg/tsdb/postgres/postgres.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index fdf09216e51..5ca333fe633 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -53,7 +53,11 @@ func generateConnectionString(datasource *models.DataSource) string {
 	}
 
 	sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full")
-	u := &url.URL{Scheme: "postgres", User: url.UserPassword(datasource.User, password), Host: datasource.Url, Path: datasource.Database, RawQuery: "sslmode=" + sslmode}
+	u := &url.URL{Scheme: "postgres",
+		User: url.UserPassword(datasource.User, password),
+		Host: datasource.Url, Path: datasource.Database,
+		RawQuery: "sslmode=" + url.QueryEscape(sslmode)}
+
 	return u.String()
 }
 

From 34a8864601a5c50ca6ce737f6ec5533be1963d1e Mon Sep 17 00:00:00 2001
From: Daniel Lee <dan.limerick@gmail.com>
Date: Sun, 22 Jul 2018 21:52:26 +0200
Subject: [PATCH 049/380] changelog: adds note for #11487

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 130fbbb7945..e53b3a904a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
 * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
 * **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
+* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
 
 ### Minor
 

From 8c52e2cd5703632b568225c87f311cc27b604e54 Mon Sep 17 00:00:00 2001
From: Kim Christensen <kimworking@gmail.com>
Date: Mon, 23 Jul 2018 10:05:46 +0200
Subject: [PATCH 050/380] Fix timezone issues in test

---
 .../plugins/datasource/influxdb/specs/datasource.jest.ts    | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts b/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts
index 6ccbf843dd5..10974cdad97 100644
--- a/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts
@@ -19,8 +19,8 @@ describe('InfluxDataSource', () => {
     let query = 'SELECT max(value) FROM measurement WHERE $timeFilter';
     let queryOptions: any = {
       range: {
-        from: '2018-01-01 00:00:00',
-        to: '2018-01-02 00:00:00',
+        from: '2018-01-01T00:00:00Z',
+        to: '2018-01-02T00:00:00Z',
       },
     };
     let requestQuery;
@@ -47,7 +47,7 @@ describe('InfluxDataSource', () => {
     });
 
     it('should replace $timefilter', () => {
-      expect(requestQuery).toMatch('time >= 1514761200000ms and time <= 1514847600000ms');
+      expect(requestQuery).toMatch('time >= 1514764800000ms and time <= 1514851200000ms');
     });
   });
 });

From fb4546b8119e0120952f593ea7db4e99dd29de9c Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Mon, 23 Jul 2018 17:58:54 +0900
Subject: [PATCH 051/380] Id validation of CloudWatch GetMetricData

---
 .../datasource/cloudwatch/partials/query.parameter.html   | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html b/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
index 57a59f80265..7da6e7d2a83 100644
--- a/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
+++ b/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
@@ -33,8 +33,12 @@
 
 <div class="gf-form-inline" ng-if="target.statistics.length === 1">
 	<div class="gf-form">
-		<label class=" gf-form-label query-keyword width-8 ">Id</label>
-		<input type="text " class="gf-form-input " ng-model="target.id " spellcheck='false' ng-model-onblur ng-change="onChange() ">
+		<label class=" gf-form-label query-keyword width-8 ">
+			Id
+			<info-popover mode="right-normal ">Id can include numbers, letters, and underscore, and must start with a lowercase letter.</info-popover>
+		</label>
+		<input type="text " class="gf-form-input " ng-model="target.id " spellcheck='false' ng-pattern='/^[a-z][A-Z0-9_]*/' ng-model-onblur
+		 ng-change="onChange() ">
 	</div>
 	<div class="gf-form max-width-30 ">
 		<label class="gf-form-label query-keyword width-7 ">Expression</label>

From ae935bf08b14c1457b4f96580048003c494b8063 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 23 Jul 2018 11:06:30 +0200
Subject: [PATCH 052/380] Add jest test file

---
 .../panel/graph/specs/graph_ctrl.jest.ts      | 81 +++++++++++++++++++
 1 file changed, 81 insertions(+)
 create mode 100644 public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts

diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
new file mode 100644
index 00000000000..bd5a69f28dd
--- /dev/null
+++ b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
@@ -0,0 +1,81 @@
+// import { describe, beforeEach, it, expect, angularMocks } from '../../../../../test/lib/common';
+
+import moment from 'moment';
+import { GraphCtrl } from '../module';
+
+describe('GraphCtrl', function() {
+  let ctx = <any>{};
+
+  beforeEach(() => {
+    ctx.ctrl = new GraphCtrl({}, {}, {});
+  });
+
+  //   beforeEach(angularMocks.module('grafana.services'));
+  //   beforeEach(angularMocks.module('grafana.controllers'));
+  //   beforeEach(
+  //     angularMocks.module(function($compileProvider) {
+  //       $compileProvider.preAssignBindingsEnabled(true);
+  //     })
+  //   );
+
+  //   beforeEach(ctx.providePhase());
+  //   beforeEach(ctx.createPanelController(GraphCtrl));
+  beforeEach(() => {
+    ctx.ctrl.annotationsPromise = Promise.resolve({});
+    ctx.ctrl.updateTimeRange();
+  });
+
+  describe('when time series are outside range', function() {
+    beforeEach(function() {
+      var data = [
+        {
+          target: 'test.cpu1',
+          datapoints: [[45, 1234567890], [60, 1234567899]],
+        },
+      ];
+
+      ctx.ctrl.range = { from: moment().valueOf(), to: moment().valueOf() };
+      ctx.ctrl.onDataReceived(data);
+    });
+
+    it('should set datapointsOutside', function() {
+      expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range');
+    });
+  });
+
+  describe('when time series are inside range', function() {
+    beforeEach(function() {
+      var range = {
+        from: moment()
+          .subtract(1, 'days')
+          .valueOf(),
+        to: moment().valueOf(),
+      };
+
+      var data = [
+        {
+          target: 'test.cpu1',
+          datapoints: [[45, range.from + 1000], [60, range.from + 10000]],
+        },
+      ];
+
+      ctx.ctrl.range = range;
+      ctx.ctrl.onDataReceived(data);
+    });
+
+    it('should set datapointsOutside', function() {
+      expect(ctx.ctrl.dataWarning).toBe(null);
+    });
+  });
+
+  describe('datapointsCount given 2 series', function() {
+    beforeEach(function() {
+      var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
+      ctx.ctrl.onDataReceived(data);
+    });
+
+    it('should set datapointsCount warning', function() {
+      expect(ctx.ctrl.dataWarning.title).toBe('No data points');
+    });
+  });
+});

From ee2eda615e4eac174907752911f486ebc5310ef9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20W=C4=99grzynek?=
 <michal.wegrzynek@malloc.com.pl>
Date: Mon, 23 Jul 2018 12:07:54 +0200
Subject: [PATCH 053/380] Update kbn.ts

---
 public/app/core/utils/kbn.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts
index 463025567cd..4fc4829811f 100644
--- a/public/app/core/utils/kbn.ts
+++ b/public/app/core/utils/kbn.ts
@@ -449,6 +449,7 @@ kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
 kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
 kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
 kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF');
+kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł');
 
 // Data (Binary)
 kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
@@ -880,6 +881,7 @@ kbn.getUnitFormats = function() {
         { text: 'Swedish Krona (kr)', value: 'currencySEK' },
         { text: 'Czech koruna (czk)', value: 'currencyCZK' },
         { text: 'Swiss franc (CHF)', value: 'currencyCHF' },
+        { text: 'Polish ZÅ‚oty (PLN)', value: 'currencyPLN' },
       ],
     },
     {

From 0fa98a812bac189c107a17ba7c1cb15050800fda Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 23 Jul 2018 13:13:18 +0200
Subject: [PATCH 054/380] changelog: add notes about closing #12691

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e53b3a904a3..5cf8602824b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
+* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 
 # 5.2.2 (unreleased)
 

From ed8568f0dffcad022309e48e4b837ecd0414b69d Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 23 Jul 2018 13:38:16 +0200
Subject: [PATCH 055/380] Add graph_ctrl jest

---
 .../panel/graph/specs/graph_ctrl.jest.ts      | 42 ++++++----
 .../panel/graph/specs/graph_ctrl_specs.ts     | 78 -------------------
 2 files changed, 29 insertions(+), 91 deletions(-)
 delete mode 100644 public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts

diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
index bd5a69f28dd..a778697527f 100644
--- a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
+++ b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
@@ -1,25 +1,41 @@
-// import { describe, beforeEach, it, expect, angularMocks } from '../../../../../test/lib/common';
-
 import moment from 'moment';
 import { GraphCtrl } from '../module';
 
+jest.mock('../graph', () => ({}));
+
 describe('GraphCtrl', function() {
+  let injector = {
+    get: () => {
+      return {
+        timeRange: () => {
+          return {
+            from: '',
+            to: '',
+          };
+        },
+      };
+    },
+  };
+
+  let scope = {
+    $on: function() {},
+  };
+
+  GraphCtrl.prototype.panel = {
+    events: {
+      on: function() {},
+    },
+    gridPos: {
+      w: 100,
+    },
+  };
+
   let ctx = <any>{};
 
   beforeEach(() => {
-    ctx.ctrl = new GraphCtrl({}, {}, {});
+    ctx.ctrl = new GraphCtrl(scope, injector, {});
   });
 
-  //   beforeEach(angularMocks.module('grafana.services'));
-  //   beforeEach(angularMocks.module('grafana.controllers'));
-  //   beforeEach(
-  //     angularMocks.module(function($compileProvider) {
-  //       $compileProvider.preAssignBindingsEnabled(true);
-  //     })
-  //   );
-
-  //   beforeEach(ctx.providePhase());
-  //   beforeEach(ctx.createPanelController(GraphCtrl));
   beforeEach(() => {
     ctx.ctrl.annotationsPromise = Promise.resolve({});
     ctx.ctrl.updateTimeRange();
diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
deleted file mode 100644
index d5cefb345cf..00000000000
--- a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { describe, beforeEach, it, expect, angularMocks } from '../../../../../test/lib/common';
-
-import moment from 'moment';
-import { GraphCtrl } from '../module';
-import helpers from '../../../../../test/specs/helpers';
-
-describe('GraphCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
-
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
-
-  beforeEach(ctx.providePhase());
-  beforeEach(ctx.createPanelController(GraphCtrl));
-  beforeEach(() => {
-    ctx.ctrl.annotationsPromise = Promise.resolve({});
-    ctx.ctrl.updateTimeRange();
-  });
-
-  describe('when time series are outside range', function() {
-    beforeEach(function() {
-      var data = [
-        {
-          target: 'test.cpu1',
-          datapoints: [[45, 1234567890], [60, 1234567899]],
-        },
-      ];
-
-      ctx.ctrl.range = { from: moment().valueOf(), to: moment().valueOf() };
-      ctx.ctrl.onDataReceived(data);
-    });
-
-    it('should set datapointsOutside', function() {
-      expect(ctx.ctrl.dataWarning.title).to.be('Data points outside time range');
-    });
-  });
-
-  describe('when time series are inside range', function() {
-    beforeEach(function() {
-      var range = {
-        from: moment()
-          .subtract(1, 'days')
-          .valueOf(),
-        to: moment().valueOf(),
-      };
-
-      var data = [
-        {
-          target: 'test.cpu1',
-          datapoints: [[45, range.from + 1000], [60, range.from + 10000]],
-        },
-      ];
-
-      ctx.ctrl.range = range;
-      ctx.ctrl.onDataReceived(data);
-    });
-
-    it('should set datapointsOutside', function() {
-      expect(ctx.ctrl.dataWarning).to.be(null);
-    });
-  });
-
-  describe('datapointsCount given 2 series', function() {
-    beforeEach(function() {
-      var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
-      ctx.ctrl.onDataReceived(data);
-    });
-
-    it('should set datapointsCount warning', function() {
-      expect(ctx.ctrl.dataWarning.title).to.be('No data points');
-    });
-  });
-});

From 529883b61d43fefac03b578e1fe86b4259e9c2de Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 23 Jul 2018 13:39:32 +0200
Subject: [PATCH 056/380] Change to arrow functions

---
 public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
index a778697527f..788ca1840ba 100644
--- a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
+++ b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
@@ -18,12 +18,12 @@ describe('GraphCtrl', function() {
   };
 
   let scope = {
-    $on: function() {},
+    $on: () => {},
   };
 
   GraphCtrl.prototype.panel = {
     events: {
-      on: function() {},
+      on: () => {},
     },
     gridPos: {
       w: 100,

From 46e31621b071e36f658788c5b8f9c9ab11ca1aab Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 23 Jul 2018 14:28:17 +0200
Subject: [PATCH 057/380] Add jest file

---
 .../influxdb/specs/query_ctrl.jest.ts         | 211 ++++++++++++++++++
 1 file changed, 211 insertions(+)
 create mode 100644 public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts

diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
new file mode 100644
index 00000000000..e4dd5b226f4
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
@@ -0,0 +1,211 @@
+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() {
+  let uiSegmentSrv = {
+    newPlusButton: () => {},
+  };
+
+  let ctx = <any>{
+    dataSource: {
+      metricFindQuery: jest.fn(() => Promise.resolve([])),
+    },
+  };
+
+  InfluxQueryCtrl.prototype.panelCtrl = {
+    panel: {
+      targets: [{}],
+    },
+  };
+
+  //   beforeEach(angularMocks.module('grafana.core'));
+  //   beforeEach(angularMocks.module('grafana.controllers'));
+  //   beforeEach(angularMocks.module('grafana.services'));
+  //   beforeEach(
+  //     angularMocks.module(function($compileProvider) {
+  //       $compileProvider.preAssignBindingsEnabled(true);
+  //     })
+  //   );
+  //   beforeEach(ctx.providePhase());
+
+  //   beforeEach(
+  //     angularMocks.inject(($rootScope, $controller, $q) => {
+  //       ctx.$q = $q;
+  //       ctx.scope = $rootScope.$new();
+  //       ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
+  //       ctx.target = { target: {} };
+  //       ctx.panelCtrl = {
+  //         panel: {
+  //           targets: [ctx.target],
+  //         },
+  //       };
+  //       ctx.panelCtrl.refresh = sinon.spy();
+  //       ctx.ctrl = $controller(
+  //         InfluxQueryCtrl,
+  //         { $scope: ctx.scope },
+  //         {
+  //           panelCtrl: ctx.panelCtrl,
+  //           target: ctx.target,
+  //           datasource: ctx.datasource,
+  //         }
+  //       );
+  //     })
+  //   );
+
+  beforeEach(() => {
+    ctx.ctrl = new InfluxQueryCtrl({}, {}, {}, {}, uiSegmentSrv);
+  });
+
+  describe('init', function() {
+    it('should init tagSegments', function() {
+      expect(ctx.ctrl.tagSegments.length).toBe(1);
+    });
+
+    it('should init measurementSegment', function() {
+      expect(ctx.ctrl.measurementSegment.value).toBe('select measurement');
+    });
+  });
+
+  describe('when first tag segment is updated', function() {
+    beforeEach(function() {
+      ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+    });
+
+    it('should update tag key', function() {
+      expect(ctx.ctrl.target.tags[0].key).toBe('asd');
+      expect(ctx.ctrl.tagSegments[0].type).toBe('key');
+    });
+
+    it('should add tagSegments', function() {
+      expect(ctx.ctrl.tagSegments.length).toBe(3);
+    });
+  });
+
+  describe('when last tag value segment is updated', function() {
+    beforeEach(function() {
+      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.ctrl.target.tags[0].value).toBe('server1');
+    });
+
+    it('should set tag operator', function() {
+      expect(ctx.ctrl.target.tags[0].operator).toBe('=');
+    });
+
+    it('should add plus button for another filter', function() {
+      expect(ctx.ctrl.tagSegments[3].fake).toBe(true);
+    });
+  });
+
+  describe('when last tag value segment is updated to regex', function() {
+    beforeEach(function() {
+      ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      ctx.ctrl.tagSegmentUpdated({ value: '/server.*/', type: 'value' }, 2);
+    });
+
+    it('should update operator', function() {
+      expect(ctx.ctrl.tagSegments[1].value).toBe('=~');
+      expect(ctx.ctrl.target.tags[0].operator).toBe('=~');
+    });
+  });
+
+  describe('when second tag key is added', function() {
+    beforeEach(function() {
+      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.ctrl.target.tags[1].key).toBe('key2');
+    });
+
+    it('should add AND segment', function() {
+      expect(ctx.ctrl.tagSegments[3].value).toBe('AND');
+    });
+  });
+
+  describe('when condition is changed', function() {
+    beforeEach(function() {
+      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.ctrl.target.tags[1].condition).toBe('OR');
+    });
+
+    it('should update AND segment', function() {
+      expect(ctx.ctrl.tagSegments[3].value).toBe('OR');
+      expect(ctx.ctrl.tagSegments.length).toBe(7);
+    });
+  });
+
+  describe('when deleting first tag filter after value is selected', function() {
+    beforeEach(function() {
+      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.ctrl.target.tags.length).toBe(0);
+    });
+
+    it('should remove all segment after 2 and replace with plus button', function() {
+      expect(ctx.ctrl.tagSegments.length).toBe(1);
+      expect(ctx.ctrl.tagSegments[0].type).toBe('plus-button');
+    });
+  });
+
+  describe('when deleting second tag value before second tag value is complete', function() {
+    beforeEach(function() {
+      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.ctrl.tagSegments.length).toBe(4);
+      expect(ctx.ctrl.tagSegments[3].type).toBe('plus-button');
+    });
+  });
+
+  describe('when deleting second tag value before second tag value is complete', function() {
+    beforeEach(function() {
+      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.ctrl.tagSegments.length).toBe(4);
+      expect(ctx.ctrl.tagSegments[3].type).toBe('plus-button');
+    });
+  });
+
+  describe('when deleting second tag value after second tag filter is complete', function() {
+    beforeEach(function() {
+      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.ctrl.tagSegments.length).toBe(4);
+      expect(ctx.ctrl.tagSegments[3].type).toBe('plus-button');
+    });
+  });
+});

From 6b6a23ff6a24c62955b48c9794c0b99023ceb608 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 12 Jul 2018 15:32:32 +0200
Subject: [PATCH 058/380] Add support for interval in query variable

Add range to scopedVars

Add basic tests and extract function for range vars

Add support for range query variable in createQuery

Template vars squash
---
 .../datasource/prometheus/datasource.ts       | 20 ++++++++-
 .../prometheus/specs/datasource.jest.ts       | 43 ++++++++++++++++++-
 2 files changed, 60 insertions(+), 3 deletions(-)

diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 69ce6f440c5..75a946d6f36 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -196,13 +196,14 @@ export class PrometheusDatasource {
     var intervalFactor = target.intervalFactor || 1;
     // Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
     var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
-    var scopedVars = options.scopedVars;
+    var scopedVars = { ...options.scopedVars, ...this.getRangeScopedVars() };
     // If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
     if (interval !== adjustedInterval) {
       interval = adjustedInterval;
       scopedVars = Object.assign({}, options.scopedVars, {
         __interval: { text: interval + 's', value: interval + 's' },
         __interval_ms: { text: interval * 1000, value: interval * 1000 },
+        ...this.getRangeScopedVars(),
       });
     }
     query.step = interval;
@@ -285,11 +286,26 @@ export class PrometheusDatasource {
       return this.$q.when([]);
     }
 
-    let interpolated = this.templateSrv.replace(query, {}, this.interpolateQueryExpr);
+    let scopedVars = {
+      __interval: { text: this.interval, value: this.interval },
+      __interval_ms: { text: kbn.interval_to_ms(this.interval), value: kbn.interval_to_ms(this.interval) },
+      ...this.getRangeScopedVars(),
+    };
+    let interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr);
     var metricFindQuery = new PrometheusMetricFindQuery(this, interpolated, this.timeSrv);
     return metricFindQuery.process();
   }
 
+  getRangeScopedVars() {
+    let range = this.timeSrv.timeRange();
+    let msRange = range.to.diff(range.from);
+    let regularRange = kbn.secondsToHms(msRange / 1000);
+    return {
+      __range_ms: { text: msRange, value: msRange },
+      __range: { text: regularRange, value: regularRange },
+    };
+  }
+
   annotationQuery(options) {
     var annotation = options.annotation;
     var expr = annotation.expr || '';
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index 15798a33cd2..b8b2b50f590 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -2,6 +2,7 @@ import _ from 'lodash';
 import moment from 'moment';
 import q from 'q';
 import { alignRange, PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
+jest.mock('../metric_find_query');
 
 describe('PrometheusDatasource', () => {
   let ctx: any = {};
@@ -18,7 +19,14 @@ describe('PrometheusDatasource', () => {
   ctx.templateSrvMock = {
     replace: a => a,
   };
-  ctx.timeSrvMock = {};
+  ctx.timeSrvMock = {
+    timeRange: () => {
+      return {
+        from: moment(1531468681),
+        to: moment(1531489712),
+      };
+    },
+  };
 
   beforeEach(() => {
     ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
@@ -204,4 +212,37 @@ describe('PrometheusDatasource', () => {
       expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
     });
   });
+
+  describe('metricFindQuery', () => {
+    beforeEach(() => {
+      let query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
+      ctx.templateSrvMock.replace = jest.fn();
+      ctx.timeSrvMock.timeRange = () => {
+        return {
+          from: moment(1531468681),
+          to: moment(1531489712),
+        };
+      };
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
+      ctx.ds.metricFindQuery(query);
+    });
+
+    it('should call templateSrv.replace with scopedVars', () => {
+      expect(ctx.templateSrvMock.replace.mock.calls[0][1]).toBeDefined();
+    });
+
+    it('should have the correct range and range_ms', () => {
+      let range = ctx.templateSrvMock.replace.mock.calls[0][1].__range;
+      let rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms;
+      expect(range).toEqual({ text: '21s', value: '21s' });
+      expect(rangeMs).toEqual({ text: 21031, value: 21031 });
+    });
+
+    it('should pass the default interval value', () => {
+      let interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval;
+      let intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms;
+      expect(interval).toEqual({ text: '15s', value: '15s' });
+      expect(intervalMs).toEqual({ text: 15000, value: 15000 });
+    });
+  });
 });

From bb0af52d34b201a960d3ace19a54e1b44be8748b Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 23 Jul 2018 14:54:58 +0200
Subject: [PATCH 059/380] Figuring out why it doesn't initialize

---
 .../app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts   | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
index e4dd5b226f4..c3b8d3ae20d 100644
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
@@ -16,8 +16,9 @@ describe('InfluxDBQueryCtrl', function() {
   };
 
   InfluxQueryCtrl.prototype.panelCtrl = {
+    target: { target: {} },
     panel: {
-      targets: [{}],
+      targets: [this.target],
     },
   };
 

From 76bc02b3fae41bae9b5a3643a503566332d4c267 Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Mon, 23 Jul 2018 14:58:11 +0200
Subject: [PATCH 060/380] Update CHANGELOG.md

Added #12597
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cf8602824b..58570c89c18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
 * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
+* **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)

From 816ee82d2695157cbd969f43623ae686b683f08d Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 23 Jul 2018 15:25:59 +0200
Subject: [PATCH 061/380] Add docs about global variables in query template
 variables

---
 docs/sources/features/datasources/prometheus.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md
index 4ff0baee108..190220fb0f1 100644
--- a/docs/sources/features/datasources/prometheus.md
+++ b/docs/sources/features/datasources/prometheus.md
@@ -75,6 +75,9 @@ Name | Description
 
 For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
 
+
+It is possible to use some global template variables in Prometheus query template variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, where `$__range` is the dashboard's current time range and `$__range_ms` is the current range in milliseconds.
+
 ### Using variables in queries
 
 There are two syntaxes:

From 70575c8f7816f90b074d7f65226b70e334786958 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 23 Jul 2018 15:34:03 +0200
Subject: [PATCH 062/380] Add templating docs for

---
 docs/sources/reference/templating.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index efe9db61e3d..08a142d3636 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -273,6 +273,9 @@ The `$__timeFilter` is used in the MySQL data source.
 
 This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias.
 
+### The $__range Variable
+Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond representation called `$__range_ms`.
+
 ## Repeating Panels
 
 Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want

From 47bec0fd91f42cb28b87c0130088ed667149cb70 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 23 Jul 2018 15:42:47 +0200
Subject: [PATCH 063/380] Fix requested changes

---
 .../panel/graph/specs/graph_ctrl.jest.ts      | 23 ++++++++-----------
 1 file changed, 10 insertions(+), 13 deletions(-)

diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
index 788ca1840ba..3ebcf6cdf31 100644
--- a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
+++ b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
@@ -3,7 +3,7 @@ import { GraphCtrl } from '../module';
 
 jest.mock('../graph', () => ({}));
 
-describe('GraphCtrl', function() {
+describe('GraphCtrl', () => {
   let injector = {
     get: () => {
       return {
@@ -34,15 +34,12 @@ describe('GraphCtrl', function() {
 
   beforeEach(() => {
     ctx.ctrl = new GraphCtrl(scope, injector, {});
-  });
-
-  beforeEach(() => {
     ctx.ctrl.annotationsPromise = Promise.resolve({});
     ctx.ctrl.updateTimeRange();
   });
 
-  describe('when time series are outside range', function() {
-    beforeEach(function() {
+  describe('when time series are outside range', () => {
+    beforeEach(() => {
       var data = [
         {
           target: 'test.cpu1',
@@ -54,13 +51,13 @@ describe('GraphCtrl', function() {
       ctx.ctrl.onDataReceived(data);
     });
 
-    it('should set datapointsOutside', function() {
+    it('should set datapointsOutside', () => {
       expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range');
     });
   });
 
-  describe('when time series are inside range', function() {
-    beforeEach(function() {
+  describe('when time series are inside range', () => {
+    beforeEach(() => {
       var range = {
         from: moment()
           .subtract(1, 'days')
@@ -79,18 +76,18 @@ describe('GraphCtrl', function() {
       ctx.ctrl.onDataReceived(data);
     });
 
-    it('should set datapointsOutside', function() {
+    it('should set datapointsOutside', () => {
       expect(ctx.ctrl.dataWarning).toBe(null);
     });
   });
 
-  describe('datapointsCount given 2 series', function() {
-    beforeEach(function() {
+  describe('datapointsCount given 2 series', () => {
+    beforeEach(() => {
       var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
       ctx.ctrl.onDataReceived(data);
     });
 
-    it('should set datapointsCount warning', function() {
+    it('should set datapointsCount warning', () => {
       expect(ctx.ctrl.dataWarning.title).toBe('No data points');
     });
   });

From 6b071054a31cbf55eb7e62499b91ece784e4432f Mon Sep 17 00:00:00 2001
From: srid12 <sridharreddy.kr@gmail.com>
Date: Mon, 23 Jul 2018 19:53:26 +0530
Subject: [PATCH 064/380] changing callback fn into arrow functions for correct
 usage of this (#12673)

---
 public/app/plugins/datasource/opentsdb/datasource.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts
index 39ad6c64e11..07ec4a794ec 100644
--- a/public/app/plugins/datasource/opentsdb/datasource.ts
+++ b/public/app/plugins/datasource/opentsdb/datasource.ts
@@ -480,17 +480,17 @@ export default class OpenTsDatasource {
 
   mapMetricsToTargets(metrics, options, tsdbVersion) {
     var interpolatedTagValue, arrTagV;
-    return _.map(metrics, function(metricData) {
+    return _.map(metrics, metricData => {
       if (tsdbVersion === 3) {
         return metricData.query.index;
       } else {
-        return _.findIndex(options.targets, function(target) {
+        return _.findIndex(options.targets, target => {
           if (target.filters && target.filters.length > 0) {
             return target.metric === metricData.metric;
           } else {
             return (
               target.metric === metricData.metric &&
-              _.every(target.tags, function(tagV, tagK) {
+              _.every(target.tags, (tagV, tagK) => {
                 interpolatedTagValue = this.templateSrv.replace(tagV, options.scopedVars, 'pipe');
                 arrTagV = interpolatedTagValue.split('|');
                 return _.includes(arrTagV, metricData.tags[tagK]) || interpolatedTagValue === '*';

From d9bf89438325c01a0fe5f3205b4cefff25930c40 Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Tue, 24 Jul 2018 16:58:48 +0900
Subject: [PATCH 065/380] return 400 if user input error

---
 pkg/api/metrics.go                |  2 +-
 pkg/tsdb/cloudwatch/cloudwatch.go | 21 +++++++++++++++++----
 2 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go
index c1b8ffe595e..00ad25ab8c2 100644
--- a/pkg/api/metrics.go
+++ b/pkg/api/metrics.go
@@ -52,7 +52,7 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
 		if res.Error != nil {
 			res.ErrorString = res.Error.Error()
 			resp.Message = res.ErrorString
-			statusCode = 500
+			statusCode = 400
 		}
 	}
 
diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index 38fbac3aa29..4af73fc2ba9 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -17,6 +17,7 @@ import (
 	"golang.org/x/sync/errgroup"
 
 	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
@@ -100,7 +101,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 
 		query, err := parseQuery(queryContext.Queries[i].Model)
 		if err != nil {
-			return nil, err
+			result.Results[query.RefId] = &tsdb.QueryResult{
+				Error: err,
+			}
+			return result, nil
 		}
 		query.RefId = queryContext.Queries[i].RefId
 
@@ -113,15 +117,21 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 		}
 
 		if query.Id == "" && query.Expression != "" {
-			return nil, fmt.Errorf("Invalid query: id should be set if using expression")
+			result.Results[query.RefId] = &tsdb.QueryResult{
+				Error: fmt.Errorf("Invalid query: id should be set if using expression"),
+			}
+			return result, nil
 		}
 
 		eg.Go(func() error {
 			queryRes, err := e.executeQuery(ectx, query, queryContext)
-			if err != nil {
+			if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
 				return err
 			}
 			result.Results[queryRes.RefId] = queryRes
+			if err != nil {
+				result.Results[queryRes.RefId].Error = err
+			}
 			return nil
 		})
 	}
@@ -131,11 +141,14 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 			q := getMetricDataQuery
 			eg.Go(func() error {
 				queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
-				if err != nil {
+				if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
 					return err
 				}
 				for _, queryRes := range queryResponses {
 					result.Results[queryRes.RefId] = queryRes
+					if err != nil {
+						result.Results[queryRes.RefId].Error = err
+					}
 				}
 				return nil
 			})

From 59c17053990203e6f303b5dfbdb3aa4b20611e75 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Tue, 24 Jul 2018 10:34:11 +0200
Subject: [PATCH 066/380] docs: mentation that config changes requires restart.

---
 docs/sources/installation/configuration.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index e3db7a1d60b..2a799b044b3 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -15,6 +15,8 @@ weight = 1
 The Grafana back-end has a number of configuration options that can be
 specified in a `.ini` configuration file or specified using environment variables.
 
+> **Note.** Grafana needs to be restarted for any configuration changes to take effect.
+
 ## Comments In .ini Files
 
 Semicolons (the `;` char) are the standard way to comment out lines in a `.ini` file.

From 93e73919e814b6d583aa1f3666c22cf922faaa55 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 11:03:46 +0200
Subject: [PATCH 067/380] fix code style

---
 pkg/tsdb/postgres/postgres.go | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index 5ca333fe633..f19e4fb54f4 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -53,10 +53,12 @@ func generateConnectionString(datasource *models.DataSource) string {
 	}
 
 	sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full")
-	u := &url.URL{Scheme: "postgres",
-		User: url.UserPassword(datasource.User, password),
-		Host: datasource.Url, Path: datasource.Database,
-		RawQuery: "sslmode=" + url.QueryEscape(sslmode)}
+	u := &url.URL{
+		Scheme: "postgres",
+		User:   url.UserPassword(datasource.User, password),
+		Host:   datasource.Url, Path: datasource.Database,
+		RawQuery: "sslmode=" + url.QueryEscape(sslmode),
+	}
 
 	return u.String()
 }

From 35efb7c225ae35758ab1826e7ad0012f5ddf46a8 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 11:26:09 +0200
Subject: [PATCH 068/380] changelog: add notes about closing #12644

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58570c89c18..160aab9b91a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
+* **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)

From 81c32780b905fa92ab874e4fac86395f0155f14a Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 24 Jul 2018 11:27:53 +0200
Subject: [PATCH 069/380] Pass more tests

---
 .../plugins/datasource/influxdb/query_ctrl.ts |   1 -
 .../influxdb/specs/query_ctrl.jest.ts         | 110 ++++++++++--------
 2 files changed, 60 insertions(+), 51 deletions(-)

diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts
index ce669c9f458..2be1ecc7bff 100644
--- a/public/app/plugins/datasource/influxdb/query_ctrl.ts
+++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts
@@ -22,7 +22,6 @@ export class InfluxQueryCtrl extends QueryCtrl {
   /** @ngInject **/
   constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) {
     super($scope, $injector);
-
     this.target = this.target;
     this.queryModel = new InfluxQuery(this.target, templateSrv, this.panel.scopedVars);
     this.queryBuilder = new InfluxQueryBuilder(this.target, this.datasource.database);
diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
index c3b8d3ae20d..139efbc3afa 100644
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
@@ -4,29 +4,28 @@ import 'app/core/services/segment_srv';
 // import helpers from 'test/specs/helpers';
 import { InfluxQueryCtrl } from '../query_ctrl';
 
-describe('InfluxDBQueryCtrl', function() {
+describe('InfluxDBQueryCtrl', () => {
   let uiSegmentSrv = {
     newPlusButton: () => {},
+    newKey: key => key,
+    newKeyValue: key => key,
+    newSegment: seg => seg,
+    newSelectMeasurement: () => {
+      return { value: 'select measurement' };
+    },
+    newOperator: op => op,
+    newFake: () => {},
   };
 
   let ctx = <any>{
-    dataSource: {
-      metricFindQuery: jest.fn(() => Promise.resolve([])),
-    },
-  };
-
-  InfluxQueryCtrl.prototype.panelCtrl = {
-    target: { target: {} },
-    panel: {
-      targets: [this.target],
-    },
+    dataSource: {},
   };
 
   //   beforeEach(angularMocks.module('grafana.core'));
   //   beforeEach(angularMocks.module('grafana.controllers'));
   //   beforeEach(angularMocks.module('grafana.services'));
   //   beforeEach(
-  //     angularMocks.module(function($compileProvider) {
+  //     angularMocks.module(($ =>compileProvider) {
   //       $compileProvider.preAssignBindingsEnabled(true);
   //     })
   //   );
@@ -56,147 +55,158 @@ describe('InfluxDBQueryCtrl', function() {
   //     })
   //   );
 
-  beforeEach(() => {
-    ctx.ctrl = new InfluxQueryCtrl({}, {}, {}, {}, uiSegmentSrv);
+  beforeEach(async () => {
+    InfluxQueryCtrl.prototype.datasource = {
+      metricFindQuery: jest.fn(() => Promise.resolve([])),
+    };
+    InfluxQueryCtrl.prototype.panelCtrl = {
+      panel: {
+        targets: [InfluxQueryCtrl.target],
+      },
+    };
+
+    InfluxQueryCtrl.prototype.target = { target: {} };
+    console.log('creating new instance');
+    ctx.ctrl = await new InfluxQueryCtrl({}, {}, {}, {}, uiSegmentSrv);
   });
 
-  describe('init', function() {
-    it('should init tagSegments', function() {
+  describe('init', () => {
+    it('should init tagSegments', () => {
       expect(ctx.ctrl.tagSegments.length).toBe(1);
     });
 
-    it('should init measurementSegment', function() {
+    it('should init measurementSegment', () => {
       expect(ctx.ctrl.measurementSegment.value).toBe('select measurement');
     });
   });
 
-  describe('when first tag segment is updated', function() {
-    beforeEach(function() {
+  describe('when first tag segment is updated', () => {
+    beforeEach(() => {
       ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
     });
 
-    it('should update tag key', function() {
+    it('should update tag key', () => {
       expect(ctx.ctrl.target.tags[0].key).toBe('asd');
       expect(ctx.ctrl.tagSegments[0].type).toBe('key');
     });
 
-    it('should add tagSegments', function() {
+    it('should add tagSegments', () => {
       expect(ctx.ctrl.tagSegments.length).toBe(3);
     });
   });
 
-  describe('when last tag value segment is updated', function() {
-    beforeEach(function() {
+  describe('when last tag value segment is updated', () => {
+    beforeEach(() => {
       ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
       ctx.ctrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
     });
 
-    it('should update tag value', function() {
+    it('should update tag value', () => {
       expect(ctx.ctrl.target.tags[0].value).toBe('server1');
     });
 
-    it('should set tag operator', function() {
+    it('should set tag operator', () => {
       expect(ctx.ctrl.target.tags[0].operator).toBe('=');
     });
 
-    it('should add plus button for another filter', function() {
+    it('should add plus button for another filter', () => {
       expect(ctx.ctrl.tagSegments[3].fake).toBe(true);
     });
   });
 
-  describe('when last tag value segment is updated to regex', function() {
-    beforeEach(function() {
+  describe('when last tag value segment is updated to regex', () => {
+    beforeEach(() => {
       ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
       ctx.ctrl.tagSegmentUpdated({ value: '/server.*/', type: 'value' }, 2);
     });
 
-    it('should update operator', function() {
+    it('should update operator', () => {
       expect(ctx.ctrl.tagSegments[1].value).toBe('=~');
       expect(ctx.ctrl.target.tags[0].operator).toBe('=~');
     });
   });
 
-  describe('when second tag key is added', function() {
-    beforeEach(function() {
+  describe('when second tag key is added', () => {
+    beforeEach(() => {
       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() {
+    it('should update tag key', () => {
       expect(ctx.ctrl.target.tags[1].key).toBe('key2');
     });
 
-    it('should add AND segment', function() {
+    it('should add AND segment', () => {
       expect(ctx.ctrl.tagSegments[3].value).toBe('AND');
     });
   });
 
-  describe('when condition is changed', function() {
-    beforeEach(function() {
+  describe('when condition is changed', () => {
+    beforeEach(() => {
       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() {
+    it('should update tag condition', () => {
       expect(ctx.ctrl.target.tags[1].condition).toBe('OR');
     });
 
-    it('should update AND segment', function() {
+    it('should update AND segment', () => {
       expect(ctx.ctrl.tagSegments[3].value).toBe('OR');
       expect(ctx.ctrl.tagSegments.length).toBe(7);
     });
   });
 
-  describe('when deleting first tag filter after value is selected', function() {
-    beforeEach(function() {
+  describe('when deleting first tag filter after value is selected', () => {
+    beforeEach(() => {
       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() {
+    it('should remove tags', () => {
       expect(ctx.ctrl.target.tags.length).toBe(0);
     });
 
-    it('should remove all segment after 2 and replace with plus button', function() {
+    it('should remove all segment after 2 and replace with plus button', () => {
       expect(ctx.ctrl.tagSegments.length).toBe(1);
       expect(ctx.ctrl.tagSegments[0].type).toBe('plus-button');
     });
   });
 
-  describe('when deleting second tag value before second tag value is complete', function() {
-    beforeEach(function() {
+  describe('when deleting second tag value before second tag value is complete', () => {
+    beforeEach(() => {
       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() {
+    it('should remove all segment after 2 and replace with plus button', () => {
       expect(ctx.ctrl.tagSegments.length).toBe(4);
       expect(ctx.ctrl.tagSegments[3].type).toBe('plus-button');
     });
   });
 
-  describe('when deleting second tag value before second tag value is complete', function() {
-    beforeEach(function() {
+  describe('when deleting second tag value before second tag value is complete', () => {
+    beforeEach(() => {
       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() {
+    it('should remove all segment after 2 and replace with plus button', () => {
       expect(ctx.ctrl.tagSegments.length).toBe(4);
       expect(ctx.ctrl.tagSegments[3].type).toBe('plus-button');
     });
   });
 
-  describe('when deleting second tag value after second tag filter is complete', function() {
-    beforeEach(function() {
+  describe('when deleting second tag value after second tag filter is complete', () => {
+    beforeEach(() => {
       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);
@@ -204,7 +214,7 @@ describe('InfluxDBQueryCtrl', function() {
       ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 4);
     });
 
-    it('should remove all segment after 2 and replace with plus button', function() {
+    it('should remove all segment after 2 and replace with plus button', () => {
       expect(ctx.ctrl.tagSegments.length).toBe(4);
       expect(ctx.ctrl.tagSegments[3].type).toBe('plus-button');
     });

From 987a16086bbafeccf3c07a5099e5b3ddf914102b Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 24 Jul 2018 14:34:37 +0200
Subject: [PATCH 070/380] Karma to Jest

---
 .../influxdb/specs/query_ctrl.jest.ts         | 70 ++++---------------
 1 file changed, 14 insertions(+), 56 deletions(-)

diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
index 139efbc3afa..6b929432dfa 100644
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
@@ -1,73 +1,31 @@
 import '../query_ctrl';
-import 'app/core/services/segment_srv';
+import { uiSegmentSrv } from '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', () => {
-  let uiSegmentSrv = {
-    newPlusButton: () => {},
-    newKey: key => key,
-    newKeyValue: key => key,
-    newSegment: seg => seg,
-    newSelectMeasurement: () => {
-      return { value: 'select measurement' };
-    },
-    newOperator: op => op,
-    newFake: () => {},
-  };
+  let ctx = <any>{};
 
-  let ctx = <any>{
-    dataSource: {},
-  };
-
-  //   beforeEach(angularMocks.module('grafana.core'));
-  //   beforeEach(angularMocks.module('grafana.controllers'));
-  //   beforeEach(angularMocks.module('grafana.services'));
-  //   beforeEach(
-  //     angularMocks.module(($ =>compileProvider) {
-  //       $compileProvider.preAssignBindingsEnabled(true);
-  //     })
-  //   );
-  //   beforeEach(ctx.providePhase());
-
-  //   beforeEach(
-  //     angularMocks.inject(($rootScope, $controller, $q) => {
-  //       ctx.$q = $q;
-  //       ctx.scope = $rootScope.$new();
-  //       ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-  //       ctx.target = { target: {} };
-  //       ctx.panelCtrl = {
-  //         panel: {
-  //           targets: [ctx.target],
-  //         },
-  //       };
-  //       ctx.panelCtrl.refresh = sinon.spy();
-  //       ctx.ctrl = $controller(
-  //         InfluxQueryCtrl,
-  //         { $scope: ctx.scope },
-  //         {
-  //           panelCtrl: ctx.panelCtrl,
-  //           target: ctx.target,
-  //           datasource: ctx.datasource,
-  //         }
-  //       );
-  //     })
-  //   );
-
-  beforeEach(async () => {
+  beforeEach(() => {
     InfluxQueryCtrl.prototype.datasource = {
-      metricFindQuery: jest.fn(() => Promise.resolve([])),
+      metricFindQuery: () => Promise.resolve([]),
     };
+    InfluxQueryCtrl.prototype.target = { target: {} };
     InfluxQueryCtrl.prototype.panelCtrl = {
       panel: {
-        targets: [InfluxQueryCtrl.target],
+        targets: [InfluxQueryCtrl.prototype.target],
       },
+      refresh: () => {},
     };
 
-    InfluxQueryCtrl.prototype.target = { target: {} };
-    console.log('creating new instance');
-    ctx.ctrl = await new InfluxQueryCtrl({}, {}, {}, {}, uiSegmentSrv);
+    ctx.ctrl = new InfluxQueryCtrl(
+      {},
+      {},
+      {},
+      {},
+      new uiSegmentSrv({ trustAsHtml: html => html }, { highlightVariablesAsHtml: () => {} })
+    );
   });
 
   describe('init', () => {

From 48ae9ec77ebbc5e3b1546a795af1f8fded555ff4 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 24 Jul 2018 14:35:37 +0200
Subject: [PATCH 071/380] Remove comments and Karm test

---
 .../influxdb/specs/query_ctrl.jest.ts         |   2 -
 .../influxdb/specs/query_ctrl_specs.ts        | 193 ------------------
 2 files changed, 195 deletions(-)
 delete mode 100644 public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts

diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
index 6b929432dfa..4e3fc47a5fd 100644
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
@@ -1,7 +1,5 @@
 import '../query_ctrl';
 import { uiSegmentSrv } from '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', () => {
diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
deleted file mode 100644
index 4daa48d6b9d..00000000000
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-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();
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
-  beforeEach(ctx.providePhase());
-
-  beforeEach(
-    angularMocks.inject(($rootScope, $controller, $q) => {
-      ctx.$q = $q;
-      ctx.scope = $rootScope.$new();
-      ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-      ctx.target = { target: {} };
-      ctx.panelCtrl = {
-        panel: {
-          targets: [ctx.target],
-        },
-      };
-      ctx.panelCtrl.refresh = sinon.spy();
-      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.ctrl.tagSegments.length).to.be(1);
-    });
-
-    it('should init measurementSegment', function() {
-      expect(ctx.ctrl.measurementSegment.value).to.be('select measurement');
-    });
-  });
-
-  describe('when first tag segment is updated', function() {
-    beforeEach(function() {
-      ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-    });
-
-    it('should update tag key', function() {
-      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.ctrl.tagSegments.length).to.be(3);
-    });
-  });
-
-  describe('when last tag value segment is updated', function() {
-    beforeEach(function() {
-      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.ctrl.target.tags[0].value).to.be('server1');
-    });
-
-    it('should set tag operator', function() {
-      expect(ctx.ctrl.target.tags[0].operator).to.be('=');
-    });
-
-    it('should add plus button for another filter', function() {
-      expect(ctx.ctrl.tagSegments[3].fake).to.be(true);
-    });
-  });
-
-  describe('when last tag value segment is updated to regex', function() {
-    beforeEach(function() {
-      ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      ctx.ctrl.tagSegmentUpdated({ value: '/server.*/', type: 'value' }, 2);
-    });
-
-    it('should update operator', function() {
-      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.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.ctrl.target.tags[1].key).to.be('key2');
-    });
-
-    it('should add AND segment', function() {
-      expect(ctx.ctrl.tagSegments[3].value).to.be('AND');
-    });
-  });
-
-  describe('when condition is changed', function() {
-    beforeEach(function() {
-      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.ctrl.target.tags[1].condition).to.be('OR');
-    });
-
-    it('should update AND segment', function() {
-      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.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.ctrl.target.tags.length).to.be(0);
-    });
-
-    it('should remove all segment after 2 and replace with plus button', function() {
-      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.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.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.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.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.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.ctrl.tagSegments.length).to.be(4);
-      expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
-    });
-  });
-});

From c0f9c06f2163dc57424257b204e6c6c449aa0212 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 29 Jun 2018 13:37:21 +0200
Subject: [PATCH 072/380] Karma to Jest: completer

---
 .../{completer_specs.ts => completer.jest.ts} | 70 +++++++++----------
 1 file changed, 34 insertions(+), 36 deletions(-)
 rename public/app/plugins/datasource/prometheus/specs/{completer_specs.ts => completer.jest.ts} (79%)

diff --git a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts b/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
similarity index 79%
rename from public/app/plugins/datasource/prometheus/specs/completer_specs.ts
rename to public/app/plugins/datasource/prometheus/specs/completer.jest.ts
index 84694834089..cb8dd8e5bd6 100644
--- a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts
+++ b/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
@@ -1,47 +1,45 @@
-import { describe, it, sinon, expect } from 'test/lib/common';
-import helpers from 'test/specs/helpers';
+//import { describe, it, sinon, expect } from 'test/lib/common';
+//import helpers from 'test/specs/helpers';
 
 import { PromCompleter } from '../completer';
 import { PrometheusDatasource } from '../datasource';
+import { BackendSrv } from 'app/core/services/backend_srv';
+jest.mock('../datasource');
+jest.mock('app/core/services/backend_srv');
 
 describe('Prometheus editor completer', function() {
-  var ctx = new helpers.ServiceTestContext();
-  beforeEach(ctx.providePhase(['templateSrv']));
+  //beforeEach(ctx.providePhase(['templateSrv']));
 
   function getSessionStub(data) {
     return {
-      getTokenAt: sinon.stub().returns(data.currentToken),
-      getTokens: sinon.stub().returns(data.tokens),
-      getLine: sinon.stub().returns(data.line),
+      getTokenAt:jest.fn(()=> (data.currentToken)),
+      getTokens:jest.fn(()=> (data.tokens)),
+      getLine:jest.fn(()=> (data.line)),
     };
   }
 
   let editor = {};
-  let datasourceStub = <PrometheusDatasource>{
-    performInstantQuery: sinon
-      .stub()
-      .withArgs({ expr: '{__name__="node_cpu"' })
-      .returns(
-        Promise.resolve({
-          data: {
+
+  let backendSrv = <BackendSrv>{}
+  let datasourceStub = new PrometheusDatasource({},{},backendSrv,{},{});
+
+  datasourceStub.performInstantQuery = jest.fn(() => Promise.resolve({
             data: {
-              result: [
-                {
-                  metric: {
-                    job: 'node',
-                    instance: 'localhost:9100',
+              data: {
+                result: [
+                  {
+                    metric: {
+                      job: 'node',
+                      instance: 'localhost:9100',
+                    },
                   },
-                },
-              ],
+                ],
+              },
             },
-          },
-        })
-      ),
-    performSuggestQuery: sinon
-      .stub()
-      .withArgs('node', true)
-      .returns(Promise.resolve(['node_cpu'])),
-  };
+          })
+        );
+  datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu']));
+
 
   let templateSrv = {
     variables: [
@@ -62,9 +60,9 @@ describe('Prometheus editor completer', function() {
       });
 
       return completer.getCompletions(editor, session, { row: 0, column: 10 }, '[', (s, res) => {
-        expect(res[0].caption).to.eql('$__interval');
-        expect(res[0].value).to.eql('[$__interval');
-        expect(res[0].meta).to.eql('range vector');
+        expect(res[0].caption).toEqual('$__interval');
+        expect(res[0].value).toEqual('[$__interval');
+        expect(res[0].meta).toEqual('range vector');
       });
     });
   });
@@ -93,7 +91,7 @@ describe('Prometheus editor completer', function() {
       });
 
       return completer.getCompletions(editor, session, { row: 0, column: 10 }, 'j', (s, res) => {
-        expect(res[0].meta).to.eql('label name');
+        expect(res[0].meta).toEqual('label name');
       });
     });
   });
@@ -125,7 +123,7 @@ describe('Prometheus editor completer', function() {
       });
 
       return completer.getCompletions(editor, session, { row: 0, column: 23 }, 'j', (s, res) => {
-        expect(res[0].meta).to.eql('label name');
+        expect(res[0].meta).toEqual('label name');
       });
     });
   });
@@ -156,7 +154,7 @@ describe('Prometheus editor completer', function() {
       });
 
       return completer.getCompletions(editor, session, { row: 0, column: 15 }, 'n', (s, res) => {
-        expect(res[0].meta).to.eql('label value');
+        expect(res[0].meta).toEqual('label value');
       });
     });
   });
@@ -192,7 +190,7 @@ describe('Prometheus editor completer', function() {
       });
 
       return completer.getCompletions(editor, session, { row: 0, column: 23 }, 'm', (s, res) => {
-        expect(res[0].meta).to.eql('label name');
+        expect(res[0].meta).toEqual('label name');
       });
     });
   });

From 49a8c2e0c138118f4e1bc3bfa37446eba596b98c Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 29 Jun 2018 13:44:11 +0200
Subject: [PATCH 073/380] Make beautiful

---
 .../prometheus/specs/completer.jest.ts        | 38 +++++++++----------
 1 file changed, 19 insertions(+), 19 deletions(-)

diff --git a/public/app/plugins/datasource/prometheus/specs/completer.jest.ts b/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
index cb8dd8e5bd6..b401cb9bf65 100644
--- a/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
@@ -12,35 +12,35 @@ describe('Prometheus editor completer', function() {
 
   function getSessionStub(data) {
     return {
-      getTokenAt:jest.fn(()=> (data.currentToken)),
-      getTokens:jest.fn(()=> (data.tokens)),
-      getLine:jest.fn(()=> (data.line)),
+      getTokenAt: jest.fn(() => data.currentToken),
+      getTokens: jest.fn(() => data.tokens),
+      getLine: jest.fn(() => data.line),
     };
   }
 
   let editor = {};
 
-  let backendSrv = <BackendSrv>{}
-  let datasourceStub = new PrometheusDatasource({},{},backendSrv,{},{});
+  let backendSrv = <BackendSrv>{};
+  let datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {});
 
-  datasourceStub.performInstantQuery = jest.fn(() => Promise.resolve({
-            data: {
-              data: {
-                result: [
-                  {
-                    metric: {
-                      job: 'node',
-                      instance: 'localhost:9100',
-                    },
-                  },
-                ],
+  datasourceStub.performInstantQuery = jest.fn(() =>
+    Promise.resolve({
+      data: {
+        data: {
+          result: [
+            {
+              metric: {
+                job: 'node',
+                instance: 'localhost:9100',
               },
             },
-          })
-        );
+          ],
+        },
+      },
+    })
+  );
   datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu']));
 
-
   let templateSrv = {
     variables: [
       {

From d2f81d52d4b121cbc0bc6c39527900a4c5cf2042 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 2 Jul 2018 09:43:34 +0200
Subject: [PATCH 074/380] Karma to Jest: begin influx query_ctrl

---
 .../influxdb/specs/query_ctrl.jest.ts         | 222 ++++++++++++++++++
 .../influxdb/specs/query_ctrl_specs.ts        | 193 ---------------
 2 files changed, 222 insertions(+), 193 deletions(-)
 create mode 100644 public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
 delete mode 100644 public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts

diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
new file mode 100644
index 00000000000..dd6c9b4fa18
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
@@ -0,0 +1,222 @@
+import '../query_ctrl';
+import 'app/core/services/segment_srv';
+import { uiSegmentSrv } from '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', () => {
+  //var ctx = new helpers.ControllerTestContext();
+
+  // beforeEach(angularMocks.module('grafana.core'));
+  // beforeEach(angularMocks.module('grafana.controllers'));
+  // beforeEach(angularMocks.module('grafana.services'));
+  // beforeEach(
+  //   angularMocks.module(($ =>compileProvider) {
+  //     $compileProvider.preAssignBindingsEnabled(true);
+  //   })
+  // );
+  // beforeEach(ctx.providePhase());
+
+  // beforeEach(
+  //   angularMocks.inject(($rootScope, $controller, $q) => {
+  //     ctx.$q = $q;
+  //     ctx.scope = $rootScope.$new();
+  //     ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
+  //     ctx.target = { target: {} };
+  //     ctx.panelCtrl = {
+  //       panel: {
+  //         targets: [ctx.target],
+  //       },
+  //     };
+  //     ctx.panelCtrl.refresh = sinon.spy();
+  //     influxQueryCtrl = $controller(
+  //       InfluxQueryCtrl,
+  //       { $scope: ctx.scope },
+  //       {
+  //         panelCtrl: ctx.panelCtrl,
+  //         target: ctx.target,
+  //         datasource: ctx.datasource,
+  //       }
+  //     );
+  //   })
+  // );
+
+  InfluxQueryCtrl.prototype.target = { target: {} };
+  InfluxQueryCtrl.prototype.panelCtrl = {
+    refresh: jest.fn(),
+    panel: {
+      targets: InfluxQueryCtrl.prototype.target,
+    },
+  };
+  InfluxQueryCtrl.prototype.datasource = {
+    metricFindQuery: jest.fn(() => Promise.resolve([])),
+  };
+
+  // let uiSegmentSrv = {
+  //   newPlusButton: jest.fn(),
+  //   newSegment: jest.fn(),
+  //   newSelectMeasurement: jest.fn()
+  // };
+  let influxQueryCtrl;
+
+  beforeEach(() => {
+    influxQueryCtrl = new InfluxQueryCtrl(
+      {},
+      {},
+      {},
+      {},
+      new uiSegmentSrv({ trustAsHtml: jest.fn() }, { highlightVariablesAsHtml: jest.fn() })
+    );
+  });
+  describe('init', () => {
+    it('should init tagSegments', () => {
+      expect(influxQueryCtrl.tagSegments.length).toBe(1);
+    });
+
+    it('should init measurementSegment', () => {
+      expect(influxQueryCtrl.measurementSegment.value).toBe('select measurement');
+    });
+  });
+
+  describe('when first tag segment is updated', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+    });
+
+    it('should update tag key', () => {
+      expect(influxQueryCtrl.target.tags[0].key).toBe('asd');
+      expect(influxQueryCtrl.tagSegments[0].type).toBe('key');
+    });
+
+    it('should add tagSegments', () => {
+      console.log(influxQueryCtrl.tagSegments);
+      expect(influxQueryCtrl.tagSegments.length).toBe(3);
+    });
+  });
+
+  describe('when last tag value segment is updated', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
+    });
+
+    it('should update tag value', () => {
+      expect(influxQueryCtrl.target.tags[0].value).toBe('server1');
+    });
+
+    it('should set tag operator', () => {
+      expect(influxQueryCtrl.target.tags[0].operator).toBe('=');
+    });
+
+    it('should add plus button for another filter', () => {
+      expect(influxQueryCtrl.tagSegments[3].fake).toBe(true);
+    });
+  });
+
+  describe('when last tag value segment is updated to regex', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      influxQueryCtrl.tagSegmentUpdated({ value: '/server.*/', type: 'value' }, 2);
+    });
+
+    it('should update operator', () => {
+      expect(influxQueryCtrl.tagSegments[1].value).toBe('=~');
+      expect(influxQueryCtrl.target.tags[0].operator).toBe('=~');
+    });
+  });
+
+  describe('when second tag key is added', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
+    });
+
+    it('should update tag key', () => {
+      expect(influxQueryCtrl.target.tags[1].key).toBe('key2');
+    });
+
+    it('should add AND segment', () => {
+      expect(influxQueryCtrl.tagSegments[3].value).toBe('AND');
+    });
+  });
+
+  describe('when condition is changed', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'OR', type: 'condition' }, 3);
+    });
+
+    it('should update tag condition', () => {
+      expect(influxQueryCtrl.target.tags[1].condition).toBe('OR');
+    });
+
+    it('should update AND segment', () => {
+      expect(influxQueryCtrl.tagSegments[3].value).toBe('OR');
+      expect(influxQueryCtrl.tagSegments.length).toBe(7);
+    });
+  });
+
+  describe('when deleting first tag filter after value is selected', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
+      influxQueryCtrl.tagSegmentUpdated(influxQueryCtrl.removeTagFilterSegment, 0);
+    });
+
+    it('should remove tags', () => {
+      expect(influxQueryCtrl.target.tags.length).toBe(0);
+    });
+
+    it('should remove all segment after 2 and replace with plus button', () => {
+      expect(influxQueryCtrl.tagSegments.length).toBe(1);
+      expect(influxQueryCtrl.tagSegments[0].type).toBe('plus-button');
+    });
+  });
+
+  describe('when deleting second tag value before second tag value is complete', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
+      influxQueryCtrl.tagSegmentUpdated(influxQueryCtrl.removeTagFilterSegment, 4);
+    });
+
+    it('should remove all segment after 2 and replace with plus button', () => {
+      expect(influxQueryCtrl.tagSegments.length).toBe(4);
+      expect(influxQueryCtrl.tagSegments[3].type).toBe('plus-button');
+    });
+  });
+
+  describe('when deleting second tag value before second tag value is complete', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
+      influxQueryCtrl.tagSegmentUpdated(influxQueryCtrl.removeTagFilterSegment, 4);
+    });
+
+    it('should remove all segment after 2 and replace with plus button', () => {
+      expect(influxQueryCtrl.tagSegments.length).toBe(4);
+      expect(influxQueryCtrl.tagSegments[3].type).toBe('plus-button');
+    });
+  });
+
+  describe('when deleting second tag value after second tag filter is complete', () => {
+    beforeEach(() => {
+      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
+      influxQueryCtrl.tagSegmentUpdated({ value: 'value', type: 'value' }, 6);
+      influxQueryCtrl.tagSegmentUpdated(influxQueryCtrl.removeTagFilterSegment, 4);
+    });
+
+    it('should remove all segment after 2 and replace with plus button', () => {
+      expect(influxQueryCtrl.tagSegments.length).toBe(4);
+      expect(influxQueryCtrl.tagSegments[3].type).toBe('plus-button');
+    });
+  });
+});
diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
deleted file mode 100644
index 4daa48d6b9d..00000000000
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-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();
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
-  beforeEach(ctx.providePhase());
-
-  beforeEach(
-    angularMocks.inject(($rootScope, $controller, $q) => {
-      ctx.$q = $q;
-      ctx.scope = $rootScope.$new();
-      ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-      ctx.target = { target: {} };
-      ctx.panelCtrl = {
-        panel: {
-          targets: [ctx.target],
-        },
-      };
-      ctx.panelCtrl.refresh = sinon.spy();
-      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.ctrl.tagSegments.length).to.be(1);
-    });
-
-    it('should init measurementSegment', function() {
-      expect(ctx.ctrl.measurementSegment.value).to.be('select measurement');
-    });
-  });
-
-  describe('when first tag segment is updated', function() {
-    beforeEach(function() {
-      ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-    });
-
-    it('should update tag key', function() {
-      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.ctrl.tagSegments.length).to.be(3);
-    });
-  });
-
-  describe('when last tag value segment is updated', function() {
-    beforeEach(function() {
-      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.ctrl.target.tags[0].value).to.be('server1');
-    });
-
-    it('should set tag operator', function() {
-      expect(ctx.ctrl.target.tags[0].operator).to.be('=');
-    });
-
-    it('should add plus button for another filter', function() {
-      expect(ctx.ctrl.tagSegments[3].fake).to.be(true);
-    });
-  });
-
-  describe('when last tag value segment is updated to regex', function() {
-    beforeEach(function() {
-      ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      ctx.ctrl.tagSegmentUpdated({ value: '/server.*/', type: 'value' }, 2);
-    });
-
-    it('should update operator', function() {
-      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.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.ctrl.target.tags[1].key).to.be('key2');
-    });
-
-    it('should add AND segment', function() {
-      expect(ctx.ctrl.tagSegments[3].value).to.be('AND');
-    });
-  });
-
-  describe('when condition is changed', function() {
-    beforeEach(function() {
-      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.ctrl.target.tags[1].condition).to.be('OR');
-    });
-
-    it('should update AND segment', function() {
-      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.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.ctrl.target.tags.length).to.be(0);
-    });
-
-    it('should remove all segment after 2 and replace with plus button', function() {
-      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.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.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.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.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.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.ctrl.tagSegments.length).to.be(4);
-      expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
-    });
-  });
-});

From d6381bed7cebe7c0270bf0ddacc8333e17fb9658 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 2 Jul 2018 14:34:58 +0200
Subject: [PATCH 075/380] Test fail depending on test order

---
 .../plugins/datasource/influxdb/query_ctrl.ts |   2 +-
 .../influxdb/specs/query_ctrl.jest.ts         |   4 +-
 .../influxdb/specs/query_ctrl_specs.ts        | 195 ++++++++++++++++++
 3 files changed, 198 insertions(+), 3 deletions(-)
 create mode 100644 public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts

diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts
index ce669c9f458..17449711143 100644
--- a/public/app/plugins/datasource/influxdb/query_ctrl.ts
+++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts
@@ -338,7 +338,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
         this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
       }
     }
-
+    console.log(this.tagSegments);
     this.rebuildTargetTagConditions();
   }
 
diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
index dd6c9b4fa18..0c1ed3ed6b2 100644
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
@@ -46,7 +46,7 @@ describe('InfluxDBQueryCtrl', () => {
   InfluxQueryCtrl.prototype.panelCtrl = {
     refresh: jest.fn(),
     panel: {
-      targets: InfluxQueryCtrl.prototype.target,
+      targets: [InfluxQueryCtrl.prototype.target],
     },
   };
   InfluxQueryCtrl.prototype.datasource = {
@@ -69,6 +69,7 @@ describe('InfluxDBQueryCtrl', () => {
       new uiSegmentSrv({ trustAsHtml: jest.fn() }, { highlightVariablesAsHtml: jest.fn() })
     );
   });
+
   describe('init', () => {
     it('should init tagSegments', () => {
       expect(influxQueryCtrl.tagSegments.length).toBe(1);
@@ -90,7 +91,6 @@ describe('InfluxDBQueryCtrl', () => {
     });
 
     it('should add tagSegments', () => {
-      console.log(influxQueryCtrl.tagSegments);
       expect(influxQueryCtrl.tagSegments.length).toBe(3);
     });
   });
diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
new file mode 100644
index 00000000000..151dd7ab0c6
--- /dev/null
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
@@ -0,0 +1,195 @@
+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();
+
+  beforeEach(angularMocks.module('grafana.core'));
+  beforeEach(angularMocks.module('grafana.controllers'));
+  beforeEach(angularMocks.module('grafana.services'));
+  beforeEach(
+    angularMocks.module(function($compileProvider) {
+      $compileProvider.preAssignBindingsEnabled(true);
+    })
+  );
+  beforeEach(ctx.providePhase());
+
+  beforeEach(
+    angularMocks.inject(($rootScope, $controller, $q) => {
+      ctx.$q = $q;
+      ctx.scope = $rootScope.$new();
+      ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
+      ctx.target = { target: {} };
+      ctx.panelCtrl = {
+        panel: {
+          targets: [ctx.target],
+        },
+      };
+      ctx.panelCtrl.refresh = sinon.spy();
+      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.ctrl.tagSegments.length).to.be(1);
+    });
+
+    it('should init measurementSegment', function() {
+      expect(ctx.ctrl.measurementSegment.value).to.be('select measurement');
+    });
+  });
+
+  describe('when first tag segment is updated', function() {
+    beforeEach(function() {
+      ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+    });
+
+    it('should update tag key', function() {
+      console.log(ctx.ctrl.target.tags);
+      expect(ctx.ctrl.target.tags[0].key).to.be('asd');
+      expect(ctx.ctrl.tagSegments[0].type).to.be('key');
+    });
+
+    it('should add tagSegments', function() {
+      console.log(ctx.ctrl.tagSegments);
+      expect(ctx.ctrl.tagSegments.length).to.be(3);
+    });
+  });
+
+  describe('when last tag value segment is updated', function() {
+    beforeEach(function() {
+      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.ctrl.target.tags[0].value).to.be('server1');
+    });
+
+    it('should set tag operator', function() {
+      expect(ctx.ctrl.target.tags[0].operator).to.be('=');
+    });
+
+    it('should add plus button for another filter', function() {
+      expect(ctx.ctrl.tagSegments[3].fake).to.be(true);
+    });
+  });
+
+  describe('when last tag value segment is updated to regex', function() {
+    beforeEach(function() {
+      ctx.ctrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
+      ctx.ctrl.tagSegmentUpdated({ value: '/server.*/', type: 'value' }, 2);
+    });
+
+    it('should update operator', function() {
+      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.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.ctrl.target.tags[1].key).to.be('key2');
+    });
+
+    it('should add AND segment', function() {
+      expect(ctx.ctrl.tagSegments[3].value).to.be('AND');
+    });
+  });
+
+  describe('when condition is changed', function() {
+    beforeEach(function() {
+      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.ctrl.target.tags[1].condition).to.be('OR');
+    });
+
+    it('should update AND segment', function() {
+      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.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.ctrl.target.tags.length).to.be(0);
+    });
+
+    it('should remove all segment after 2 and replace with plus button', function() {
+      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.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.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.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.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.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.ctrl.tagSegments.length).to.be(4);
+      expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
+    });
+  });
+});

From 51caf470f50c07fdb7f6d47d7fe022f2ebfc1ac5 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 24 Jul 2018 14:55:54 +0200
Subject: [PATCH 076/380] Remove influx qeury_ctrl jest, as it is already
 completed

---
 .../influxdb/specs/query_ctrl.jest.ts         | 222 ------------------
 .../prometheus/specs/completer.jest.ts        |   3 -
 2 files changed, 225 deletions(-)
 delete mode 100644 public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts

diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
deleted file mode 100644
index 0c1ed3ed6b2..00000000000
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-import '../query_ctrl';
-import 'app/core/services/segment_srv';
-import { uiSegmentSrv } from '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', () => {
-  //var ctx = new helpers.ControllerTestContext();
-
-  // beforeEach(angularMocks.module('grafana.core'));
-  // beforeEach(angularMocks.module('grafana.controllers'));
-  // beforeEach(angularMocks.module('grafana.services'));
-  // beforeEach(
-  //   angularMocks.module(($ =>compileProvider) {
-  //     $compileProvider.preAssignBindingsEnabled(true);
-  //   })
-  // );
-  // beforeEach(ctx.providePhase());
-
-  // beforeEach(
-  //   angularMocks.inject(($rootScope, $controller, $q) => {
-  //     ctx.$q = $q;
-  //     ctx.scope = $rootScope.$new();
-  //     ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-  //     ctx.target = { target: {} };
-  //     ctx.panelCtrl = {
-  //       panel: {
-  //         targets: [ctx.target],
-  //       },
-  //     };
-  //     ctx.panelCtrl.refresh = sinon.spy();
-  //     influxQueryCtrl = $controller(
-  //       InfluxQueryCtrl,
-  //       { $scope: ctx.scope },
-  //       {
-  //         panelCtrl: ctx.panelCtrl,
-  //         target: ctx.target,
-  //         datasource: ctx.datasource,
-  //       }
-  //     );
-  //   })
-  // );
-
-  InfluxQueryCtrl.prototype.target = { target: {} };
-  InfluxQueryCtrl.prototype.panelCtrl = {
-    refresh: jest.fn(),
-    panel: {
-      targets: [InfluxQueryCtrl.prototype.target],
-    },
-  };
-  InfluxQueryCtrl.prototype.datasource = {
-    metricFindQuery: jest.fn(() => Promise.resolve([])),
-  };
-
-  // let uiSegmentSrv = {
-  //   newPlusButton: jest.fn(),
-  //   newSegment: jest.fn(),
-  //   newSelectMeasurement: jest.fn()
-  // };
-  let influxQueryCtrl;
-
-  beforeEach(() => {
-    influxQueryCtrl = new InfluxQueryCtrl(
-      {},
-      {},
-      {},
-      {},
-      new uiSegmentSrv({ trustAsHtml: jest.fn() }, { highlightVariablesAsHtml: jest.fn() })
-    );
-  });
-
-  describe('init', () => {
-    it('should init tagSegments', () => {
-      expect(influxQueryCtrl.tagSegments.length).toBe(1);
-    });
-
-    it('should init measurementSegment', () => {
-      expect(influxQueryCtrl.measurementSegment.value).toBe('select measurement');
-    });
-  });
-
-  describe('when first tag segment is updated', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-    });
-
-    it('should update tag key', () => {
-      expect(influxQueryCtrl.target.tags[0].key).toBe('asd');
-      expect(influxQueryCtrl.tagSegments[0].type).toBe('key');
-    });
-
-    it('should add tagSegments', () => {
-      expect(influxQueryCtrl.tagSegments.length).toBe(3);
-    });
-  });
-
-  describe('when last tag value segment is updated', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
-    });
-
-    it('should update tag value', () => {
-      expect(influxQueryCtrl.target.tags[0].value).toBe('server1');
-    });
-
-    it('should set tag operator', () => {
-      expect(influxQueryCtrl.target.tags[0].operator).toBe('=');
-    });
-
-    it('should add plus button for another filter', () => {
-      expect(influxQueryCtrl.tagSegments[3].fake).toBe(true);
-    });
-  });
-
-  describe('when last tag value segment is updated to regex', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      influxQueryCtrl.tagSegmentUpdated({ value: '/server.*/', type: 'value' }, 2);
-    });
-
-    it('should update operator', () => {
-      expect(influxQueryCtrl.tagSegments[1].value).toBe('=~');
-      expect(influxQueryCtrl.target.tags[0].operator).toBe('=~');
-    });
-  });
-
-  describe('when second tag key is added', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
-    });
-
-    it('should update tag key', () => {
-      expect(influxQueryCtrl.target.tags[1].key).toBe('key2');
-    });
-
-    it('should add AND segment', () => {
-      expect(influxQueryCtrl.tagSegments[3].value).toBe('AND');
-    });
-  });
-
-  describe('when condition is changed', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'OR', type: 'condition' }, 3);
-    });
-
-    it('should update tag condition', () => {
-      expect(influxQueryCtrl.target.tags[1].condition).toBe('OR');
-    });
-
-    it('should update AND segment', () => {
-      expect(influxQueryCtrl.tagSegments[3].value).toBe('OR');
-      expect(influxQueryCtrl.tagSegments.length).toBe(7);
-    });
-  });
-
-  describe('when deleting first tag filter after value is selected', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
-      influxQueryCtrl.tagSegmentUpdated(influxQueryCtrl.removeTagFilterSegment, 0);
-    });
-
-    it('should remove tags', () => {
-      expect(influxQueryCtrl.target.tags.length).toBe(0);
-    });
-
-    it('should remove all segment after 2 and replace with plus button', () => {
-      expect(influxQueryCtrl.tagSegments.length).toBe(1);
-      expect(influxQueryCtrl.tagSegments[0].type).toBe('plus-button');
-    });
-  });
-
-  describe('when deleting second tag value before second tag value is complete', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
-      influxQueryCtrl.tagSegmentUpdated(influxQueryCtrl.removeTagFilterSegment, 4);
-    });
-
-    it('should remove all segment after 2 and replace with plus button', () => {
-      expect(influxQueryCtrl.tagSegments.length).toBe(4);
-      expect(influxQueryCtrl.tagSegments[3].type).toBe('plus-button');
-    });
-  });
-
-  describe('when deleting second tag value before second tag value is complete', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
-      influxQueryCtrl.tagSegmentUpdated(influxQueryCtrl.removeTagFilterSegment, 4);
-    });
-
-    it('should remove all segment after 2 and replace with plus button', () => {
-      expect(influxQueryCtrl.tagSegments.length).toBe(4);
-      expect(influxQueryCtrl.tagSegments[3].type).toBe('plus-button');
-    });
-  });
-
-  describe('when deleting second tag value after second tag filter is complete', () => {
-    beforeEach(() => {
-      influxQueryCtrl.tagSegmentUpdated({ value: 'asd', type: 'plus-button' }, 0);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'server1', type: 'value' }, 2);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'key2', type: 'plus-button' }, 3);
-      influxQueryCtrl.tagSegmentUpdated({ value: 'value', type: 'value' }, 6);
-      influxQueryCtrl.tagSegmentUpdated(influxQueryCtrl.removeTagFilterSegment, 4);
-    });
-
-    it('should remove all segment after 2 and replace with plus button', () => {
-      expect(influxQueryCtrl.tagSegments.length).toBe(4);
-      expect(influxQueryCtrl.tagSegments[3].type).toBe('plus-button');
-    });
-  });
-});
diff --git a/public/app/plugins/datasource/prometheus/specs/completer.jest.ts b/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
index b401cb9bf65..fbe2dce0ce5 100644
--- a/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
@@ -1,6 +1,3 @@
-//import { describe, it, sinon, expect } from 'test/lib/common';
-//import helpers from 'test/specs/helpers';
-
 import { PromCompleter } from '../completer';
 import { PrometheusDatasource } from '../datasource';
 import { BackendSrv } from 'app/core/services/backend_srv';

From b81621b6f5019e12893fdddde32b7850aabbad61 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 15:24:44 +0200
Subject: [PATCH 077/380] changelog: add notes about closing #12636 #9827

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 160aab9b91a..4917c5998d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@
 
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Dashboard**: Dashboard links not updated when changing variables [#12506](https://github.com/grafana/grafana/issues/12506)
+* **Postgres/MySQL/MSSQL**: Fix connection leak [#12636](https://github.com/grafana/grafana/issues/12636) [#9827](https://github.com/grafana/grafana/issues/9827)
 
 # 5.2.1 (2018-06-29)
 

From 3dab4e1b52c1a4e7712abd5c20da14a4736b8ca4 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 15:27:13 +0200
Subject: [PATCH 078/380] changelog: add notes about closing #12589

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4917c5998d0..826507e1bd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Dashboard**: Dashboard links not updated when changing variables [#12506](https://github.com/grafana/grafana/issues/12506)
 * **Postgres/MySQL/MSSQL**: Fix connection leak [#12636](https://github.com/grafana/grafana/issues/12636) [#9827](https://github.com/grafana/grafana/issues/9827)
+* **Dashboard**: Remove unwanted scrollbars in embedded panels [#12589](https://github.com/grafana/grafana/issues/12589)
 
 # 5.2.1 (2018-06-29)
 

From 25c8233523d317a378f628258b86d88686b1a744 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 4 Jul 2018 09:22:39 +0200
Subject: [PATCH 079/380] Begin conversion

---
 ...query_ctrl_specs.ts => query_ctrl.jest.ts} | 89 +++++++++++--------
 1 file changed, 50 insertions(+), 39 deletions(-)
 rename public/app/plugins/datasource/graphite/specs/{query_ctrl_specs.ts => query_ctrl.jest.ts} (84%)

diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
similarity index 84%
rename from public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts
rename to public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
index b4f7718930f..776dec0a1a7 100644
--- a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts
+++ b/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
@@ -6,48 +6,59 @@ import helpers from 'test/specs/helpers';
 import { GraphiteQueryCtrl } from '../query_ctrl';
 
 describe('GraphiteQueryCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
+  
+  let datasource = {
+    metricFindQuery: jest.fn(() => Promise.resolve([])),
+    getFuncDefs: jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0'))),
+    getFuncDef: gfunc.getFuncDef,
+    waitForFuncDefsLoaded: jest.fn(() => Promise.resolve(null)),
+    createFuncInstance: gfunc.createFuncInstance,
+    
+  };
+  let ctx = {
 
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
+  };
 
-  beforeEach(ctx.providePhase());
-  beforeEach(
-    angularMocks.inject(($rootScope, $controller, $q) => {
-      ctx.$q = $q;
-      ctx.scope = $rootScope.$new();
-      ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
-      ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-      ctx.datasource.getFuncDefs = sinon.stub().returns(ctx.$q.when(gfunc.getFuncDefs('1.0')));
-      ctx.datasource.getFuncDef = gfunc.getFuncDef;
-      ctx.datasource.waitForFuncDefsLoaded = sinon.stub().returns(ctx.$q.when(null));
-      ctx.datasource.createFuncInstance = gfunc.createFuncInstance;
-      ctx.panelCtrl = { panel: {} };
-      ctx.panelCtrl = {
-        panel: {
-          targets: [ctx.target],
-        },
-      };
-      ctx.panelCtrl.refresh = sinon.spy();
+  // beforeEach(angularMocks.module('grafana.core'));
+  // beforeEach(angularMocks.module('grafana.controllers'));
+  // beforeEach(angularMocks.module('grafana.services'));
+  // beforeEach(
+  //   angularMocks.module(function($compileProvider) {
+  //     $compileProvider.preAssignBindingsEnabled(true);
+  //   })
+  // );
 
-      ctx.ctrl = $controller(
-        GraphiteQueryCtrl,
-        { $scope: ctx.scope },
-        {
-          panelCtrl: ctx.panelCtrl,
-          datasource: ctx.datasource,
-          target: ctx.target,
-        }
-      );
-      ctx.scope.$digest();
-    })
-  );
+  //beforeEach(ctx.providePhase());
+  // beforeEach(
+  //   angularMocks.inject(($rootScope, $controller, $q) => {
+  //     ctx.$q = $q;
+  //     ctx.scope = $rootScope.$new();
+  //     ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
+  //     ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
+  //     ctx.datasource.getFuncDefs = sinon.stub().returns(ctx.$q.when(gfunc.getFuncDefs('1.0')));
+  //     ctx.datasource.getFuncDef = gfunc.getFuncDef;
+  //     ctx.datasource.waitForFuncDefsLoaded = sinon.stub().returns(ctx.$q.when(null));
+  //     ctx.datasource.createFuncInstance = gfunc.createFuncInstance;
+  //     ctx.panelCtrl = { panel: {} };
+  //     ctx.panelCtrl = {
+  //       panel: {
+  //         targets: [ctx.target],
+  //       },
+  //     };
+  //     ctx.panelCtrl.refresh = sinon.spy();
+
+  //     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() {

From b58a7642dc6b3be313a30be95b455fd6141f8da9 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 24 Jul 2018 15:39:56 +0200
Subject: [PATCH 080/380] Karma to Jest

---
 .../graphite/specs/query_ctrl.jest.ts         | 271 ++++++++++--------
 1 file changed, 145 insertions(+), 126 deletions(-)

diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
index 776dec0a1a7..58cefeef6f6 100644
--- a/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
+++ b/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
@@ -1,22 +1,27 @@
-import 'app/core/services/segment_srv';
-import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common';
+import { uiSegmentSrv } from 'app/core/services/segment_srv';
+// import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common';
 
 import gfunc from '../gfunc';
-import helpers from 'test/specs/helpers';
+// import helpers from 'test/specs/helpers';
 import { GraphiteQueryCtrl } from '../query_ctrl';
 
-describe('GraphiteQueryCtrl', function() {
-  
-  let datasource = {
-    metricFindQuery: jest.fn(() => Promise.resolve([])),
-    getFuncDefs: jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0'))),
-    getFuncDef: gfunc.getFuncDef,
-    waitForFuncDefsLoaded: jest.fn(() => Promise.resolve(null)),
-    createFuncInstance: gfunc.createFuncInstance,
-    
+describe('GraphiteQueryCtrl', () => {
+  let ctx = <any>{
+    datasource: {
+      metricFindQuery: jest.fn(() => Promise.resolve([])),
+      getFuncDefs: jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0'))),
+      getFuncDef: gfunc.getFuncDef,
+      waitForFuncDefsLoaded: jest.fn(() => Promise.resolve(null)),
+      createFuncInstance: gfunc.createFuncInstance,
+    },
+    target: { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' },
+    panelCtrl: {
+      refresh: jest.fn(),
+    },
   };
-  let ctx = {
 
+  ctx.panelCtrl.panel = {
+    targets: [ctx.target],
   };
 
   // beforeEach(angularMocks.module('grafana.core'));
@@ -60,156 +65,170 @@ describe('GraphiteQueryCtrl', function() {
   //   })
   // );
 
-  describe('init', function() {
-    it('should validate metric key exists', function() {
-      expect(ctx.datasource.metricFindQuery.getCall(0).args[0]).to.be('test.prod.*');
+  beforeEach(() => {
+    GraphiteQueryCtrl.prototype.target = ctx.target;
+    GraphiteQueryCtrl.prototype.datasource = ctx.datasource;
+
+    GraphiteQueryCtrl.prototype.panelCtrl = ctx.panelCtrl;
+
+    ctx.ctrl = new GraphiteQueryCtrl(
+      {},
+      {},
+      new uiSegmentSrv({ trustAsHtml: html => html }, { highlightVariablesAsHtml: () => {} }),
+      {},
+      {}
+    );
+  });
+
+  describe('init', () => {
+    it('should validate metric key exists', () => {
+      expect(ctx.datasource.metricFindQuery.mock.calls[0][0]).toBe('test.prod.*');
     });
 
-    it('should delete last segment if no metrics are found', function() {
-      expect(ctx.ctrl.segments[2].value).to.be('select metric');
+    it('should delete last segment if no metrics are found', () => {
+      expect(ctx.ctrl.segments[2].value).toBe('select metric');
     });
 
-    it('should parse expression and build function model', function() {
-      expect(ctx.ctrl.queryModel.functions.length).to.be(2);
+    it('should parse expression and build function model', () => {
+      expect(ctx.ctrl.queryModel.functions.length).toBe(2);
     });
   });
 
-  describe('when adding function', function() {
-    beforeEach(function() {
+  describe('when adding function', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = 'test.prod.*.count';
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
       ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode'));
     });
 
-    it('should add function with correct node number', function() {
-      expect(ctx.ctrl.queryModel.functions[0].params[0]).to.be(2);
+    it('should add function with correct node number', () => {
+      expect(ctx.ctrl.queryModel.functions[0].params[0]).toBe(2);
     });
 
-    it('should update target', function() {
-      expect(ctx.ctrl.target.target).to.be('aliasByNode(test.prod.*.count, 2)');
+    it('should update target', () => {
+      expect(ctx.ctrl.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
     });
 
-    it('should call refresh', function() {
-      expect(ctx.panelCtrl.refresh.called).to.be(true);
+    it('should call refresh', () => {
+      expect(ctx.panelCtrl.refresh).toHaveBeenCalled();
     });
   });
 
-  describe('when adding function before any metric segment', function() {
-    beforeEach(function() {
+  describe('when adding function before any metric segment', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = '';
-      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([{ expandable: true }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
       ctx.ctrl.parseTarget();
       ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent'));
     });
 
-    it('should add function and remove select metric link', function() {
-      expect(ctx.ctrl.segments.length).to.be(0);
+    it('should add function and remove select metric link', () => {
+      expect(ctx.ctrl.segments.length).toBe(0);
     });
   });
 
-  describe('when initializing target without metric expression and only function', function() {
-    beforeEach(function() {
+  describe('when initializing target without metric expression and only function', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = 'asPercent(#A, #B)';
-      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
       ctx.ctrl.parseTarget();
-      ctx.scope.$digest();
     });
 
-    it('should not add select metric segment', function() {
-      expect(ctx.ctrl.segments.length).to.be(1);
+    it('should not add select metric segment', () => {
+      expect(ctx.ctrl.segments.length).toBe(1);
     });
 
-    it('should add second series ref as param', function() {
-      expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
+    it('should add second series ref as param', () => {
+      expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
     });
   });
 
-  describe('when initializing a target with single param func using variable', function() {
-    beforeEach(function() {
+  describe('when initializing a target with single param func using variable', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = 'movingAverage(prod.count, $var)';
-      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
       ctx.ctrl.parseTarget();
     });
 
-    it('should add 2 segments', function() {
-      expect(ctx.ctrl.segments.length).to.be(2);
+    it('should add 2 segments', () => {
+      expect(ctx.ctrl.segments.length).toBe(2);
     });
 
-    it('should add function param', function() {
-      expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
+    it('should add function param', () => {
+      expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
     });
   });
 
-  describe('when initializing target without metric expression and function with series-ref', function() {
-    beforeEach(function() {
+  describe('when initializing target without metric expression and function with series-ref', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = 'asPercent(metric.node.count, #A)';
-      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
       ctx.ctrl.parseTarget();
     });
 
-    it('should add segments', function() {
-      expect(ctx.ctrl.segments.length).to.be(3);
+    it('should add segments', () => {
+      expect(ctx.ctrl.segments.length).toBe(3);
     });
 
-    it('should have correct func params', function() {
-      expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
+    it('should have correct func params', () => {
+      expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
     });
   });
 
-  describe('when getting altSegments and metricFindQuery returns empty array', function() {
-    beforeEach(function() {
+  describe('when getting altSegments and metricFindQuery returns empty array', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = 'test.count';
-      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
       ctx.ctrl.parseTarget();
       ctx.ctrl.getAltSegments(1).then(function(results) {
         ctx.altSegments = results;
       });
-      ctx.scope.$digest();
     });
 
-    it('should have no segments', function() {
-      expect(ctx.altSegments.length).to.be(0);
+    it('should have no segments', () => {
+      expect(ctx.altSegments.length).toBe(0);
     });
   });
 
-  describe('targetChanged', function() {
-    beforeEach(function() {
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+  describe('targetChanged', () => {
+    beforeEach(() => {
+      ctx.ctrl.target.target = 'aliasByNode(scaleToSeconds(test.prod.*, 1), 2)';
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
       ctx.ctrl.target.target = '';
       ctx.ctrl.targetChanged();
     });
 
-    it('should rebuld target after expression model', function() {
-      expect(ctx.ctrl.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
+    it('should rebuild target after expression model', () => {
+      expect(ctx.ctrl.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
     });
 
-    it('should call panelCtrl.refresh', function() {
-      expect(ctx.panelCtrl.refresh.called).to.be(true);
+    it('should call panelCtrl.refresh', () => {
+      expect(ctx.panelCtrl.refresh).toHaveBeenCalled();
     });
   });
 
-  describe('when updating targets with nested query', function() {
-    beforeEach(function() {
+  describe('when updating targets with nested query', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
     });
 
-    it('should add function params', function() {
-      expect(ctx.ctrl.queryModel.segments.length).to.be(1);
-      expect(ctx.ctrl.queryModel.segments[0].value).to.be('#A');
+    it('should add function params', () => {
+      expect(ctx.ctrl.queryModel.segments.length).toBe(1);
+      expect(ctx.ctrl.queryModel.segments[0].value).toBe('#A');
 
-      expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
-      expect(ctx.ctrl.queryModel.functions[0].params[0]).to.be(60);
+      expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
+      expect(ctx.ctrl.queryModel.functions[0].params[0]).toBe(60);
     });
 
-    it('target should remain the same', function() {
-      expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
+    it('target should remain the same', () => {
+      expect(ctx.ctrl.target.target).toBe('scaleToSeconds(#A, 60)');
     });
 
-    it('targetFull should include nested queries', function() {
+    it('targetFull should include nested queries', () => {
       ctx.ctrl.panelCtrl.panel.targets = [
         {
           target: 'nested.query.count',
@@ -219,17 +238,17 @@ describe('GraphiteQueryCtrl', function() {
 
       ctx.ctrl.updateModelTarget();
 
-      expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
+      expect(ctx.ctrl.target.target).toBe('scaleToSeconds(#A, 60)');
 
-      expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)');
+      expect(ctx.ctrl.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
     });
   });
 
-  describe('when updating target used in other query', function() {
-    beforeEach(function() {
+  describe('when updating target used in other query', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = 'metrics.a.count';
       ctx.ctrl.target.refId = 'A';
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
 
       ctx.ctrl.panelCtrl.panel.targets = [ctx.ctrl.target, { target: 'sumSeries(#A)', refId: 'B' }];
@@ -237,113 +256,113 @@ describe('GraphiteQueryCtrl', function() {
       ctx.ctrl.updateModelTarget();
     });
 
-    it('targetFull of other query should update', function() {
-      expect(ctx.ctrl.panel.targets[1].targetFull).to.be('sumSeries(metrics.a.count)');
+    it('targetFull of other query should update', () => {
+      expect(ctx.ctrl.panel.targets[1].targetFull).toBe('sumSeries(metrics.a.count)');
     });
   });
 
-  describe('when adding seriesByTag function', function() {
-    beforeEach(function() {
+  describe('when adding seriesByTag function', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = '';
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
       ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
     });
 
-    it('should update functions', function() {
-      expect(ctx.ctrl.queryModel.getSeriesByTagFuncIndex()).to.be(0);
+    it('should update functions', () => {
+      expect(ctx.ctrl.queryModel.getSeriesByTagFuncIndex()).toBe(0);
     });
 
-    it('should update seriesByTagUsed flag', function() {
-      expect(ctx.ctrl.queryModel.seriesByTagUsed).to.be(true);
+    it('should update seriesByTagUsed flag', () => {
+      expect(ctx.ctrl.queryModel.seriesByTagUsed).toBe(true);
     });
 
-    it('should update target', function() {
-      expect(ctx.ctrl.target.target).to.be('seriesByTag()');
+    it('should update target', () => {
+      expect(ctx.ctrl.target.target).toBe('seriesByTag()');
     });
 
-    it('should call refresh', function() {
-      expect(ctx.panelCtrl.refresh.called).to.be(true);
+    it('should call refresh', () => {
+      expect(ctx.panelCtrl.refresh).toHaveBeenCalled();
     });
   });
 
-  describe('when parsing seriesByTag function', function() {
-    beforeEach(function() {
+  describe('when parsing seriesByTag function', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
     });
 
-    it('should add tags', function() {
+    it('should add tags', () => {
       const expected = [
         { key: 'tag1', operator: '=', value: 'value1' },
         { key: 'tag2', operator: '!=~', value: 'value2' },
       ];
-      expect(ctx.ctrl.queryModel.tags).to.eql(expected);
+      expect(ctx.ctrl.queryModel.tags).toEqual(expected);
     });
 
-    it('should add plus button', function() {
-      expect(ctx.ctrl.addTagSegments.length).to.be(1);
+    it('should add plus button', () => {
+      expect(ctx.ctrl.addTagSegments.length).toBe(1);
     });
   });
 
-  describe('when tag added', function() {
-    beforeEach(function() {
+  describe('when tag added', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = 'seriesByTag()';
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
       ctx.ctrl.addNewTag({ value: 'tag1' });
     });
 
-    it('should update tags with default value', function() {
+    it('should update tags with default value', () => {
       const expected = [{ key: 'tag1', operator: '=', value: '' }];
-      expect(ctx.ctrl.queryModel.tags).to.eql(expected);
+      expect(ctx.ctrl.queryModel.tags).toEqual(expected);
     });
 
-    it('should update target', function() {
+    it('should update target', () => {
       const expected = "seriesByTag('tag1=')";
-      expect(ctx.ctrl.target.target).to.eql(expected);
+      expect(ctx.ctrl.target.target).toEqual(expected);
     });
   });
 
-  describe('when tag changed', function() {
-    beforeEach(function() {
+  describe('when tag changed', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
       ctx.ctrl.tagChanged({ key: 'tag1', operator: '=', value: 'new_value' }, 0);
     });
 
-    it('should update tags', function() {
+    it('should update tags', () => {
       const expected = [
         { key: 'tag1', operator: '=', value: 'new_value' },
         { key: 'tag2', operator: '!=~', value: 'value2' },
       ];
-      expect(ctx.ctrl.queryModel.tags).to.eql(expected);
+      expect(ctx.ctrl.queryModel.tags).toEqual(expected);
     });
 
-    it('should update target', function() {
+    it('should update target', () => {
       const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
-      expect(ctx.ctrl.target.target).to.eql(expected);
+      expect(ctx.ctrl.target.target).toEqual(expected);
     });
   });
 
-  describe('when tag removed', function() {
-    beforeEach(function() {
+  describe('when tag removed', () => {
+    beforeEach(() => {
       ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
-      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
+      ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
       ctx.ctrl.parseTarget();
       ctx.ctrl.removeTag(0);
     });
 
-    it('should update tags', function() {
+    it('should update tags', () => {
       const expected = [{ key: 'tag2', operator: '!=~', value: 'value2' }];
-      expect(ctx.ctrl.queryModel.tags).to.eql(expected);
+      expect(ctx.ctrl.queryModel.tags).toEqual(expected);
     });
 
-    it('should update target', function() {
+    it('should update target', () => {
       const expected = "seriesByTag('tag2!=~value2')";
-      expect(ctx.ctrl.target.target).to.eql(expected);
+      expect(ctx.ctrl.target.target).toEqual(expected);
     });
   });
 });

From 1c691ac855142222dc4549a613d52a1171487e1d Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 15:51:34 +0200
Subject: [PATCH 081/380] changelog: add notes about closing #12533

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 826507e1bd6..0f3fb6b9d01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@
 * **Dashboard**: Dashboard links not updated when changing variables [#12506](https://github.com/grafana/grafana/issues/12506)
 * **Postgres/MySQL/MSSQL**: Fix connection leak [#12636](https://github.com/grafana/grafana/issues/12636) [#9827](https://github.com/grafana/grafana/issues/9827)
 * **Dashboard**: Remove unwanted scrollbars in embedded panels [#12589](https://github.com/grafana/grafana/issues/12589)
+* **Prometheus**: Prevent error using $__interval_ms in query [#12533](https://github.com/grafana/grafana/pull/12533)
 
 # 5.2.1 (2018-06-29)
 

From a63fca03b87193c87d6154628254998a06cf434d Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 15:57:07 +0200
Subject: [PATCH 082/380] changelog: add notes about closing #12551

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f3fb6b9d01..6a7d2db1c14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Dashboard**: Dashboard links not updated when changing variables [#12506](https://github.com/grafana/grafana/issues/12506)
 * **Postgres/MySQL/MSSQL**: Fix connection leak [#12636](https://github.com/grafana/grafana/issues/12636) [#9827](https://github.com/grafana/grafana/issues/9827)
+* **Plugins**: Fix loading of external plugins [#12551](https://github.com/grafana/grafana/issues/12551)
 * **Dashboard**: Remove unwanted scrollbars in embedded panels [#12589](https://github.com/grafana/grafana/issues/12589)
 * **Prometheus**: Prevent error using $__interval_ms in query [#12533](https://github.com/grafana/grafana/pull/12533)
 

From 5de8b6c2f01cdfa0505f93e6469a38702fdd66fa Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 16:45:36 +0200
Subject: [PATCH 083/380] changelog: add notes about closing #12489

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a7d2db1c14..aa794b92164 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
+* **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 
 # 5.2.2 (unreleased)
 

From 27c081349fb11f1ad8d304873aa9cc92a45a2027 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 24 Jul 2018 17:03:58 +0200
Subject: [PATCH 084/380] Remove old influx stuff

---
 public/app/plugins/datasource/influxdb/query_ctrl.ts            | 2 +-
 .../app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts   | 2 --
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts
index 17449711143..ce669c9f458 100644
--- a/public/app/plugins/datasource/influxdb/query_ctrl.ts
+++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts
@@ -338,7 +338,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
         this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
       }
     }
-    console.log(this.tagSegments);
+
     this.rebuildTargetTagConditions();
   }
 
diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
index 151dd7ab0c6..4daa48d6b9d 100644
--- a/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
+++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts
@@ -57,13 +57,11 @@ describe('InfluxDBQueryCtrl', function() {
     });
 
     it('should update tag key', function() {
-      console.log(ctx.ctrl.target.tags);
       expect(ctx.ctrl.target.tags[0].key).to.be('asd');
       expect(ctx.ctrl.tagSegments[0].type).to.be('key');
     });
 
     it('should add tagSegments', function() {
-      console.log(ctx.ctrl.tagSegments);
       expect(ctx.ctrl.tagSegments.length).to.be(3);
     });
   });

From d8d748d2aa9987e93e6b8988b66d2d217be98ac0 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 17:40:00 +0200
Subject: [PATCH 085/380] remove unneeded comment

---
 .../app/plugins/datasource/prometheus/specs/completer.jest.ts   | 2 --
 1 file changed, 2 deletions(-)

diff --git a/public/app/plugins/datasource/prometheus/specs/completer.jest.ts b/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
index fbe2dce0ce5..b29e4d27233 100644
--- a/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/completer.jest.ts
@@ -5,8 +5,6 @@ jest.mock('../datasource');
 jest.mock('app/core/services/backend_srv');
 
 describe('Prometheus editor completer', function() {
-  //beforeEach(ctx.providePhase(['templateSrv']));
-
   function getSessionStub(data) {
     return {
       getTokenAt: jest.fn(() => data.currentToken),

From ce9b25a5ac66f0f6a8b9a2f1c91b14c184ed9143 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 24 Jul 2018 18:30:29 +0200
Subject: [PATCH 086/380] Remove comments

---
 .../graphite/specs/query_ctrl.jest.ts         | 44 -------------------
 1 file changed, 44 deletions(-)

diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
index 58cefeef6f6..b38ad56427b 100644
--- a/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
+++ b/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
@@ -1,8 +1,5 @@
 import { uiSegmentSrv } from 'app/core/services/segment_srv';
-// import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common';
-
 import gfunc from '../gfunc';
-// import helpers from 'test/specs/helpers';
 import { GraphiteQueryCtrl } from '../query_ctrl';
 
 describe('GraphiteQueryCtrl', () => {
@@ -24,47 +21,6 @@ describe('GraphiteQueryCtrl', () => {
     targets: [ctx.target],
   };
 
-  // beforeEach(angularMocks.module('grafana.core'));
-  // beforeEach(angularMocks.module('grafana.controllers'));
-  // beforeEach(angularMocks.module('grafana.services'));
-  // beforeEach(
-  //   angularMocks.module(function($compileProvider) {
-  //     $compileProvider.preAssignBindingsEnabled(true);
-  //   })
-  // );
-
-  //beforeEach(ctx.providePhase());
-  // beforeEach(
-  //   angularMocks.inject(($rootScope, $controller, $q) => {
-  //     ctx.$q = $q;
-  //     ctx.scope = $rootScope.$new();
-  //     ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
-  //     ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-  //     ctx.datasource.getFuncDefs = sinon.stub().returns(ctx.$q.when(gfunc.getFuncDefs('1.0')));
-  //     ctx.datasource.getFuncDef = gfunc.getFuncDef;
-  //     ctx.datasource.waitForFuncDefsLoaded = sinon.stub().returns(ctx.$q.when(null));
-  //     ctx.datasource.createFuncInstance = gfunc.createFuncInstance;
-  //     ctx.panelCtrl = { panel: {} };
-  //     ctx.panelCtrl = {
-  //       panel: {
-  //         targets: [ctx.target],
-  //       },
-  //     };
-  //     ctx.panelCtrl.refresh = sinon.spy();
-
-  //     ctx.ctrl = $controller(
-  //       GraphiteQueryCtrl,
-  //       { $scope: ctx.scope },
-  //       {
-  //         panelCtrl: ctx.panelCtrl,
-  //         datasource: ctx.datasource,
-  //         target: ctx.target,
-  //       }
-  //     );
-  //     ctx.scope.$digest();
-  //   })
-  // );
-
   beforeEach(() => {
     GraphiteQueryCtrl.prototype.target = ctx.target;
     GraphiteQueryCtrl.prototype.datasource = ctx.datasource;

From 1dd9646a502c8f0749ed1752b25f39111677effb Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 19:05:09 +0200
Subject: [PATCH 087/380] fix failing test due to time diff issues

---
 pkg/services/sqlstore/dashboard_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go
index 0ca1c5d67e4..8ff78c4a0ff 100644
--- a/pkg/services/sqlstore/dashboard_test.go
+++ b/pkg/services/sqlstore/dashboard_test.go
@@ -181,7 +181,7 @@ func TestDashboardDataAccess(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(query.Result.FolderId, ShouldEqual, 0)
 				So(query.Result.CreatedBy, ShouldEqual, savedDash.CreatedBy)
-				So(query.Result.Created, ShouldEqual, savedDash.Created.Truncate(time.Second))
+				So(query.Result.Created, ShouldHappenWithin, 3*time.Second, savedDash.Created)
 				So(query.Result.UpdatedBy, ShouldEqual, 100)
 				So(query.Result.Updated.IsZero(), ShouldBeFalse)
 			})

From 582652145fa825cfce0a85b827d70f09b2cda45e Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 24 Jul 2018 19:21:23 +0200
Subject: [PATCH 088/380] minor fixes

---
 docs/sources/features/datasources/prometheus.md | 6 +++++-
 docs/sources/reference/templating.md            | 3 +++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md
index 190220fb0f1..0ed9e108df6 100644
--- a/docs/sources/features/datasources/prometheus.md
+++ b/docs/sources/features/datasources/prometheus.md
@@ -76,7 +76,11 @@ Name | Description
 For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
 
 
-It is possible to use some global template variables in Prometheus query template variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, where `$__range` is the dashboard's current time range and `$__range_ms` is the current range in milliseconds.
+#### Using interval and range variables
+
+> Support for `$__range` and `$__range_ms` only available from Grafana v5.3
+
+It's possible to use some global template variables in Prometheus query template variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, where `$__range` is the dashboard's current time range and `$__range_ms` is the current range in milliseconds.
 
 ### Using variables in queries
 
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index 08a142d3636..ce1a1299d26 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -274,6 +274,9 @@ The `$__timeFilter` is used in the MySQL data source.
 This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias.
 
 ### The $__range Variable
+
+> Only available in Grafana v5.3+
+
 Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond representation called `$__range_ms`.
 
 ## Repeating Panels

From 055d208a326f08cc4ad69324f9c4c1722b35e59e Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Wed, 25 Jul 2018 11:27:43 +0900
Subject: [PATCH 089/380] fix invalid reference

---
 pkg/tsdb/cloudwatch/cloudwatch.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index 4af73fc2ba9..92352a51315 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -99,14 +99,15 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 			continue
 		}
 
+		RefId := queryContext.Queries[i].RefId
 		query, err := parseQuery(queryContext.Queries[i].Model)
 		if err != nil {
-			result.Results[query.RefId] = &tsdb.QueryResult{
+			result.Results[RefId] = &tsdb.QueryResult{
 				Error: err,
 			}
 			return result, nil
 		}
-		query.RefId = queryContext.Queries[i].RefId
+		query.RefId = RefId
 
 		if query.Id != "" {
 			if _, ok := getMetricDataQueries[query.Region]; !ok {

From f4ab432542383c726d517f7a70000460d69ac4b3 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Wed, 25 Jul 2018 10:29:55 +0200
Subject: [PATCH 090/380] added position absolute and some flexbox so I could
 remov changes in display and setTimeout, added tests and types, did some
 renaming

---
 public/app/containers/Teams/TeamList.tsx      |  2 +-
 .../DeleteButton/DeleteButton.jest.tsx        | 44 ++++++++++
 .../components/DeleteButton/DeleteButton.tsx  | 82 ++++++++-----------
 public/sass/components/_delete_button.scss    | 37 +++++----
 4 files changed, 99 insertions(+), 66 deletions(-)
 create mode 100644 public/app/core/components/DeleteButton/DeleteButton.jest.tsx

diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx
index 87d24f8ddd4..b86763d8799 100644
--- a/public/app/containers/Teams/TeamList.tsx
+++ b/public/app/containers/Teams/TeamList.tsx
@@ -55,7 +55,7 @@ export class TeamList extends React.Component<Props, any> {
           <a href={teamUrl}>{team.memberCount}</a>
         </td>
         <td className="text-right">
-          <DeleteButton confirmDelete={() => this.deleteTeam(team)} />
+          <DeleteButton onConfirmDelete={() => this.deleteTeam(team)} />
         </td>
       </tr>
     );
diff --git a/public/app/core/components/DeleteButton/DeleteButton.jest.tsx b/public/app/core/components/DeleteButton/DeleteButton.jest.tsx
new file mode 100644
index 00000000000..12acadee18a
--- /dev/null
+++ b/public/app/core/components/DeleteButton/DeleteButton.jest.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import DeleteButton from './DeleteButton';
+import { shallow } from 'enzyme';
+
+describe('DeleteButton', () => {
+  let wrapper;
+  let deleted;
+
+  beforeAll(() => {
+    deleted = false;
+
+    function deleteItem() {
+      deleted = true;
+    }
+    wrapper = shallow(<DeleteButton onConfirmDelete={() => deleteItem()} />);
+  });
+
+  it('should show confirm delete when clicked', () => {
+    expect(wrapper.state().showConfirm).toBe(false);
+    wrapper.find('.delete-button').simulate('click');
+    expect(wrapper.state().showConfirm).toBe(true);
+  });
+
+  it('should hide confirm delete when clicked', () => {
+    wrapper.find('.delete-button').simulate('click');
+    expect(wrapper.state().showConfirm).toBe(true);
+    wrapper
+      .find('.confirm-delete')
+      .find('.btn')
+      .at(0)
+      .simulate('click');
+    expect(wrapper.state().showConfirm).toBe(false);
+  });
+
+  it('should show confirm delete when clicked', () => {
+    expect(deleted).toBe(false);
+    wrapper
+      .find('.confirm-delete')
+      .find('.btn')
+      .at(1)
+      .simulate('click');
+    expect(deleted).toBe(true);
+  });
+});
diff --git a/public/app/core/components/DeleteButton/DeleteButton.tsx b/public/app/core/components/DeleteButton/DeleteButton.tsx
index 61a322b591e..a83ce6097ad 100644
--- a/public/app/core/components/DeleteButton/DeleteButton.tsx
+++ b/public/app/core/components/DeleteButton/DeleteButton.tsx
@@ -1,73 +1,61 @@
-import React, { Component } from 'react';
+import React, { PureComponent } from 'react';
 
-export default class DeleteButton extends Component<any, any> {
-  state = {
-    deleteButton: 'delete-button--show',
-    confirmSpan: 'confirm-delete--removed',
+export interface DeleteButtonProps {
+  onConfirmDelete();
+}
+
+export interface DeleteButtonStates {
+  showConfirm: boolean;
+}
+
+export default class DeleteButton extends PureComponent<DeleteButtonProps, DeleteButtonStates> {
+  state: DeleteButtonStates = {
+    showConfirm: false,
   };
 
-  handleDelete = event => {
+  onClickDelete = event => {
     if (event) {
       event.preventDefault();
     }
 
     this.setState({
-      deleteButton: 'delete-button--hide',
+      showConfirm: true,
     });
-
-    setTimeout(() => {
-      this.setState({
-        deleteButton: 'delete-button--removed',
-      });
-    }, 100);
-
-    setTimeout(() => {
-      this.setState({
-        confirmSpan: 'confirm-delete--hide',
-      });
-    }, 100);
-
-    setTimeout(() => {
-      this.setState({
-        confirmSpan: 'confirm-delete--show',
-      });
-    }, 150);
   };
 
-  cancelDelete = event => {
-    event.preventDefault();
-
+  onClickCancel = event => {
+    if (event) {
+      event.preventDefault();
+    }
     this.setState({
-      confirmSpan: 'confirm-delete--hide',
+      showConfirm: false,
     });
-
-    setTimeout(() => {
-      this.setState({
-        confirmSpan: 'confirm-delete--removed',
-        deleteButton: 'delete-button--hide',
-      });
-    }, 140);
-
-    setTimeout(() => {
-      this.setState({
-        deleteButton: 'delete-button--show',
-      });
-    }, 190);
   };
 
   render() {
-    const { confirmDelete } = this.props;
+    const onClickConfirm = this.props.onConfirmDelete;
+    let showConfirm;
+    let showDeleteButton;
+
+    if (this.state.showConfirm) {
+      showConfirm = 'show';
+      showDeleteButton = 'hide';
+    } else {
+      showConfirm = 'hide';
+      showDeleteButton = 'show';
+    }
+
     return (
       <span className="delete-button-container">
-        <a className={this.state.deleteButton + ' btn btn-danger btn-small'} onClick={this.handleDelete}>
+        <a className={'delete-button ' + showDeleteButton + ' btn btn-danger btn-small'} onClick={this.onClickDelete}>
           <i className="fa fa-remove" />
         </a>
         <span className="confirm-delete-container">
-          <span className={this.state.confirmSpan}>
-            <a className="btn btn-small" onClick={this.cancelDelete}>
+          <span className={'confirm-delete ' + showConfirm}>
+            <a className="btn btn-small" onClick={this.onClickCancel}>
               Cancel
             </a>
-            <a className="btn btn-danger btn-small" onClick={confirmDelete}>
+            <a className="btn btn-danger btn-small" onClick={onClickConfirm}>
               Confirm Delete
             </a>
           </span>
diff --git a/public/sass/components/_delete_button.scss b/public/sass/components/_delete_button.scss
index 19f32189d81..e56a1181a09 100644
--- a/public/sass/components/_delete_button.scss
+++ b/public/sass/components/_delete_button.scss
@@ -1,49 +1,50 @@
+// sets a fixed width so that the rest of the table
+// isn't affected by the animation
 .delete-button-container {
-  max-width: 24px;
   width: 24px;
   direction: rtl;
-  max-height: 38px;
-  display: block;
+  display: flex;
+  align-items: center;
 }
 
+//this container is used to make sure confirm-delete isn't
+//shown outside of table
 .confirm-delete-container {
   overflow: hidden;
   width: 145px;
-  display: block;
+  position: absolute;
+  z-index: 1;
 }
 
 .delete-button {
-  &--show {
-    display: inline-block;
+  position: absolute;
+
+  &.show {
     opacity: 1;
     transition: opacity 0.1s ease;
+    z-index: 2;
   }
 
-  &--hide {
-    display: inline-block;
+  &.hide {
     opacity: 0;
     transition: opacity 0.1s ease;
-  }
-  &--removed {
-    display: none;
+    z-index: 0;
   }
 }
 
 .confirm-delete {
-  &--show {
-    display: inline-block;
+  display: flex;
+  align-items: flex-start;
+
+  &.show {
     opacity: 1;
     transition: opacity 0.08s ease-out, transform 0.1s ease-out;
     transform: translateX(0);
   }
 
-  &--hide {
-    display: inline-block;
+  &.hide {
     opacity: 0;
     transition: opacity 0.12s ease-in, transform 0.14s ease-in;
     transform: translateX(100px);
   }
-  &--removed {
-    display: none;
-  }
 }

From df62282c115cea465577b5f1c02077b87166255e Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Wed, 25 Jul 2018 11:27:43 +0200
Subject: [PATCH 091/380] fix for typeahead background, increased lighten

---
 public/sass/_variables.light.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss
index b6e9e7db979..b6248da6a00 100644
--- a/public/sass/_variables.light.scss
+++ b/public/sass/_variables.light.scss
@@ -218,7 +218,7 @@ $search-filter-box-bg: $gray-7;
 
 // Typeahead
 $typeahead-shadow: 0 5px 10px 0 $gray-5;
-$typeahead-selected-bg: lighten($blue, 25%);
+$typeahead-selected-bg: lighten($blue, 57%);
 $typeahead-selected-color: $blue;
 
 // Dropdowns

From 5fbd8ada3c55cfe8eecc57d894b6a445b76e00c9 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 25 Jul 2018 11:54:51 +0200
Subject: [PATCH 092/380] changelog: add notes about closing #12668

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa794b92164..27651b2216f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,6 +33,7 @@
 * **Plugins**: Fix loading of external plugins [#12551](https://github.com/grafana/grafana/issues/12551)
 * **Dashboard**: Remove unwanted scrollbars in embedded panels [#12589](https://github.com/grafana/grafana/issues/12589)
 * **Prometheus**: Prevent error using $__interval_ms in query [#12533](https://github.com/grafana/grafana/pull/12533)
+* **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 
 # 5.2.1 (2018-06-29)
 

From 45762d04e392be18658df8a0ecd081a03bb09b5f Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 25 Jul 2018 11:55:34 +0200
Subject: [PATCH 093/380] changelog: update

[skip ci]
---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27651b2216f..0f813272e60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,7 +23,7 @@
 * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 
-# 5.2.2 (unreleased)
+# 5.2.2 (2018-07-25)
 
 ### Minor
 

From 9c40028d58431fcab8c3d7dddb44b2593a0c7130 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 25 Jul 2018 13:22:55 +0200
Subject: [PATCH 094/380] changelog: add notes about closing #12668

[skip ci]
---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f813272e60..990421d30d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
+* **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 
 # 5.2.2 (2018-07-25)
 
@@ -33,7 +34,6 @@
 * **Plugins**: Fix loading of external plugins [#12551](https://github.com/grafana/grafana/issues/12551)
 * **Dashboard**: Remove unwanted scrollbars in embedded panels [#12589](https://github.com/grafana/grafana/issues/12589)
 * **Prometheus**: Prevent error using $__interval_ms in query [#12533](https://github.com/grafana/grafana/pull/12533)
-* **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 
 # 5.2.1 (2018-06-29)
 

From 7e773e2d5e35045f87be875fa81ac2c930d1257f Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 25 Jul 2018 14:14:25 +0200
Subject: [PATCH 095/380] changelog: add notes about closing #12533

[skip ci]
---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 990421d30d3..6409f094f65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,7 +33,7 @@
 * **Postgres/MySQL/MSSQL**: Fix connection leak [#12636](https://github.com/grafana/grafana/issues/12636) [#9827](https://github.com/grafana/grafana/issues/9827)
 * **Plugins**: Fix loading of external plugins [#12551](https://github.com/grafana/grafana/issues/12551)
 * **Dashboard**: Remove unwanted scrollbars in embedded panels [#12589](https://github.com/grafana/grafana/issues/12589)
-* **Prometheus**: Prevent error using $__interval_ms in query [#12533](https://github.com/grafana/grafana/pull/12533)
+* **Prometheus**: Prevent error using $__interval_ms in query [#12533](https://github.com/grafana/grafana/pull/12533), thx [@mtanda](https://github.com/mtanda)
 
 # 5.2.1 (2018-06-29)
 

From f3504612062f2bcf43a02c985942d5b70ca52439 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 25 Jul 2018 14:52:03 +0200
Subject: [PATCH 096/380] Start conversion

---
 .../specs/variable_srv_init.jest.ts           | 238 ++++++++++++++++++
 1 file changed, 238 insertions(+)
 create mode 100644 public/app/features/templating/specs/variable_srv_init.jest.ts

diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.jest.ts
new file mode 100644
index 00000000000..218170ae454
--- /dev/null
+++ b/public/app/features/templating/specs/variable_srv_init.jest.ts
@@ -0,0 +1,238 @@
+//import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common';
+
+import '../all';
+
+import _ from 'lodash';
+// import helpers from 'test/specs/helpers';
+// import { Emitter } from 'app/core/core';
+import { VariableSrv } from '../variable_srv';
+import $q from 'q';
+
+describe('VariableSrv init', function() {
+  let templateSrv = {
+    init: () => {},
+  };
+  let $injector = {
+    instantiate: (vars, model) => {
+      return new vars(model.model);
+    },
+  };
+  let $rootscope = {
+    $on: () => {},
+  };
+
+  let ctx = <any>{
+    datasourceSrv: {},
+    $location: {},
+    dashboard: {},
+  };
+
+  //   beforeEach(angularMocks.module('grafana.core'));
+  //   beforeEach(angularMocks.module('grafana.controllers'));
+  //   beforeEach(angularMocks.module('grafana.services'));
+  //   beforeEach(
+  //     angularMocks.module(function($compileProvider) {
+  //       $compileProvider.preAssignBindingsEnabled(true);
+  //     })
+  //   );
+
+  //   beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
+  //   beforeEach(
+  //     angularMocks.inject(($rootScope, $q, $location, $injector) => {
+  //       ctx.$q = $q;
+  //       ctx.$rootScope = $rootScope;
+  //       ctx.$location = $location;
+  //       ctx.variableSrv = $injector.get('variableSrv');
+  //       ctx.$rootScope.$digest();
+  //     })
+  //   );
+
+  function describeInitScenario(desc, fn) {
+    describe(desc, function() {
+      //   events: new Emitter(),
+      var scenario: any = {
+        urlParams: {},
+        setup: setupFn => {
+          scenario.setupFn = setupFn;
+        },
+      };
+
+      beforeEach(function() {
+        scenario.setupFn();
+        ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv);
+        ctx.variableSrv.datasource = {};
+        ctx.variableSrv.datasource.metricFindQuery = jest.fn(() => Promise.resolve(scenario.queryResult));
+
+        ctx.variableSrv.datasourceSrv = {
+          get: () => Promise.resolve(ctx.datasource),
+          getMetricSources: () => Promise.resolve(scenario.metricSources),
+        };
+
+        ctx.variableSrv.$location.search = () => Promise.resolve(scenario.urlParams);
+        ctx.variableSrv.dashboard = {
+          templating: { list: scenario.variables },
+          //   events: new Emitter(),
+        };
+
+        ctx.variableSrv.init(ctx.variableSrv.dashboard);
+        // ctx.$rootScope.$digest();
+
+        scenario.variables = ctx.variableSrv.variables;
+      });
+
+      fn(scenario);
+    });
+  }
+
+  ['query', 'interval', 'custom', 'datasource'].forEach(type => {
+    describeInitScenario('when setting ' + type + ' variable via url', scenario => {
+      scenario.setup(() => {
+        scenario.variables = [
+          {
+            name: 'apps',
+            type: type,
+            current: { text: 'test', value: 'test' },
+            options: [{ text: 'test', value: 'test' }],
+          },
+        ];
+        scenario.urlParams['var-apps'] = 'new';
+        scenario.metricSources = [];
+      });
+
+      it('should update current value', () => {
+        expect(scenario.variables[0].current.value).toBe('new');
+        expect(scenario.variables[0].current.text).toBe('new');
+      });
+    });
+  });
+
+  describe('given dependent variables', () => {
+    var variableList = [
+      {
+        name: 'app',
+        type: 'query',
+        query: '',
+        current: { text: 'app1', value: 'app1' },
+        options: [{ text: 'app1', value: 'app1' }],
+      },
+      {
+        name: 'server',
+        type: 'query',
+        refresh: 1,
+        query: '$app.*',
+        current: { text: 'server1', value: 'server1' },
+        options: [{ text: 'server1', value: 'server1' }],
+      },
+    ];
+
+    describeInitScenario('when setting parent var from url', scenario => {
+      scenario.setup(() => {
+        scenario.variables = _.cloneDeep(variableList);
+        scenario.urlParams['var-app'] = 'google';
+        scenario.queryResult = [{ text: 'google-server1' }, { text: 'google-server2' }];
+      });
+
+      it('should update child variable', () => {
+        expect(scenario.variables[1].options.length).toBe(2);
+        expect(scenario.variables[1].current.text).toBe('google-server1');
+      });
+
+      it('should only update it once', () => {
+        expect(ctx.variableSrv.datasource.metricFindQuery).toHaveBeenCalledTimes(1);
+      });
+    });
+  });
+
+  describeInitScenario('when datasource variable is initialized', scenario => {
+    scenario.setup(() => {
+      scenario.variables = [
+        {
+          type: 'datasource',
+          query: 'graphite',
+          name: 'test',
+          current: { value: 'backend4_pee', text: 'backend4_pee' },
+          regex: '/pee$/',
+        },
+      ];
+      scenario.metricSources = [
+        { name: 'backend1', meta: { id: 'influx' } },
+        { name: 'backend2_pee', meta: { id: 'graphite' } },
+        { name: 'backend3', meta: { id: 'graphite' } },
+        { name: 'backend4_pee', meta: { id: 'graphite' } },
+      ];
+    });
+
+    it('should update current value', function() {
+      var variable = ctx.variableSrv.variables[0];
+      expect(variable.options.length).toBe(2);
+    });
+  });
+
+  describeInitScenario('when template variable is present in url multiple times', scenario => {
+    scenario.setup(() => {
+      scenario.variables = [
+        {
+          name: 'apps',
+          type: 'query',
+          multi: true,
+          current: { text: 'val1', value: 'val1' },
+          options: [
+            { text: 'val1', value: 'val1' },
+            { text: 'val2', value: 'val2' },
+            { text: 'val3', value: 'val3', selected: true },
+          ],
+        },
+      ];
+      scenario.urlParams['var-apps'] = ['val2', 'val1'];
+    });
+
+    it('should update current value', function() {
+      var variable = ctx.variableSrv.variables[0];
+      expect(variable.current.value.length).toBe(2);
+      expect(variable.current.value[0]).toBe('val2');
+      expect(variable.current.value[1]).toBe('val1');
+      expect(variable.current.text).toBe('val2 + val1');
+      expect(variable.options[0].selected).toBe(true);
+      expect(variable.options[1].selected).toBe(true);
+    });
+
+    it('should set options that are not in value to selected false', function() {
+      var variable = ctx.variableSrv.variables[0];
+      expect(variable.options[2].selected).toBe(false);
+    });
+  });
+
+  describeInitScenario('when template variable is present in url multiple times using key/values', scenario => {
+    scenario.setup(() => {
+      scenario.variables = [
+        {
+          name: 'apps',
+          type: 'query',
+          multi: true,
+          current: { text: 'Val1', value: 'val1' },
+          options: [
+            { text: 'Val1', value: 'val1' },
+            { text: 'Val2', value: 'val2' },
+            { text: 'Val3', value: 'val3', selected: true },
+          ],
+        },
+      ];
+      scenario.urlParams['var-apps'] = ['val2', 'val1'];
+    });
+
+    it('should update current value', function() {
+      var variable = ctx.variableSrv.variables[0];
+      expect(variable.current.value.length).toBe(2);
+      expect(variable.current.value[0]).toBe('val2');
+      expect(variable.current.value[1]).toBe('val1');
+      expect(variable.current.text).toBe('Val2 + Val1');
+      expect(variable.options[0].selected).toBe(true);
+      expect(variable.options[1].selected).toBe(true);
+    });
+
+    it('should set options that are not in value to selected false', function() {
+      var variable = ctx.variableSrv.variables[0];
+      expect(variable.options[2].selected).toBe(false);
+    });
+  });
+});

From 7d51c1524007fc47dc225e1256535c1386c07aca Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 25 Jul 2018 16:15:03 +0200
Subject: [PATCH 097/380] Two passing tests

---
 .../specs/variable_srv_init.jest.ts           | 57 ++++++++++++++-----
 .../app/features/templating/variable_srv.ts   |  1 +
 2 files changed, 43 insertions(+), 15 deletions(-)

diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.jest.ts
index 218170ae454..519adc0a350 100644
--- a/public/app/features/templating/specs/variable_srv_init.jest.ts
+++ b/public/app/features/templating/specs/variable_srv_init.jest.ts
@@ -7,16 +7,18 @@ import _ from 'lodash';
 // import { Emitter } from 'app/core/core';
 import { VariableSrv } from '../variable_srv';
 import $q from 'q';
+// import { model } from 'mobx-state-tree/dist/internal';
 
 describe('VariableSrv init', function() {
   let templateSrv = {
-    init: () => {},
-  };
-  let $injector = {
-    instantiate: (vars, model) => {
-      return new vars(model.model);
+    init: vars => {
+      this.variables = vars;
     },
+    variableInitialized: () => {},
+    updateTemplateData: () => {},
+    replace: str => str,
   };
+  let $injector = <any>{};
   let $rootscope = {
     $on: () => {},
   };
@@ -57,24 +59,35 @@ describe('VariableSrv init', function() {
         },
       };
 
-      beforeEach(function() {
+      beforeEach(async () => {
         scenario.setupFn();
-        ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv);
-        ctx.variableSrv.datasource = {};
-        ctx.variableSrv.datasource.metricFindQuery = jest.fn(() => Promise.resolve(scenario.queryResult));
-
-        ctx.variableSrv.datasourceSrv = {
-          get: () => Promise.resolve(ctx.datasource),
-          getMetricSources: () => Promise.resolve(scenario.metricSources),
+        ctx = {
+          datasource: {
+            metricFindQuery: jest.fn(() => Promise.resolve(scenario.queryResult)),
+          },
+          datasourceSrv: {
+            get: () => Promise.resolve(ctx.datasource),
+            getMetricSources: () => Promise.resolve(scenario.metricSources),
+          },
+          templateSrv,
         };
 
+        ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv);
+
+        $injector.instantiate = (variable, model) => {
+          return getVarMockConstructor(variable, model, ctx);
+        };
+
+        ctx.variableSrv.datasource = ctx.datasource;
+        ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
+
         ctx.variableSrv.$location.search = () => Promise.resolve(scenario.urlParams);
         ctx.variableSrv.dashboard = {
           templating: { list: scenario.variables },
-          //   events: new Emitter(),
+          // events: new Emitter(),
         };
 
-        ctx.variableSrv.init(ctx.variableSrv.dashboard);
+        await ctx.variableSrv.init(ctx.variableSrv.dashboard);
         // ctx.$rootScope.$digest();
 
         scenario.variables = ctx.variableSrv.variables;
@@ -236,3 +249,17 @@ describe('VariableSrv init', function() {
     });
   });
 });
+
+function getVarMockConstructor(variable, model, ctx) {
+  console.log(model.model.type);
+  switch (model.model.type) {
+    case 'datasource':
+      return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
+    case 'query':
+      return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
+    case 'interval':
+      return new variable(model.model, {}, ctx.templateSrv, ctx.variableSrv);
+    default:
+      return new variable(model.model);
+  }
+}
diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts
index 8ad3c2845e2..9f6522c9b86 100644
--- a/public/app/features/templating/variable_srv.ts
+++ b/public/app/features/templating/variable_srv.ts
@@ -23,6 +23,7 @@ export class VariableSrv {
 
     // init variables
     for (let variable of this.variables) {
+      console.log(variable);
       variable.initLock = this.$q.defer();
     }
 

From 0f99e624b680b60e00ca05f408c5b85464d7cf81 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 25 Jul 2018 16:20:00 +0200
Subject: [PATCH 098/380] docs: using interval and range variables in
 prometheus

Included example usages
---
 .../features/datasources/prometheus.md        | 21 ++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md
index 0ed9e108df6..3a04ef92e31 100644
--- a/docs/sources/features/datasources/prometheus.md
+++ b/docs/sources/features/datasources/prometheus.md
@@ -80,7 +80,26 @@ For details of *metric names*, *label names* and *label values* are please refer
 
 > Support for `$__range` and `$__range_ms` only available from Grafana v5.3
 
-It's possible to use some global template variables in Prometheus query template variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, where `$__range` is the dashboard's current time range and `$__range_ms` is the current range in milliseconds.
+It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since
+`label_values` function doesn't support queries.
+
+Make sure to set the variable's `refresh` trigger to be `On Time Range Change` to get the correct instances when changing the time range on the dashboard.
+
+**Example usage:**
+
+Populate a variable with the the busiest 5 request instances based on average QPS over the time range shown in the dashboard:
+
+```
+Query: query_result(topk(5, sum(rate(http_requests_total[$__range])) by (instance)))
+Regex: /"([^"]+)"/
+```
+
+Populate a variable with the instances having a certain state over the time range shown in the dashboard:
+
+```
+Query: query_result(max_over_time(<metric>[$__range]) != <state>)
+Regex:
+```
 
 ### Using variables in queries
 

From 84e431d377b51405f37b4bae8321454218bcc7c4 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Wed, 25 Jul 2018 16:16:33 +0200
Subject: [PATCH 099/380] Add tslib to TS compiler

- using tslib reduces bundle sizes
- add compiler option for easier default imports of CJS modules
- remove double entry of fork-ts-checker-plugin
- speed up hot reload by using exprimental ts-loader API
---
 package.json                   | 16 ++++----
 scripts/webpack/webpack.hot.js | 10 ++++-
 tsconfig.json                  | 73 +++++++++++++++++++---------------
 yarn.lock                      |  8 +++-
 4 files changed, 65 insertions(+), 42 deletions(-)

diff --git a/package.json b/package.json
index c26438230cc..c0581c1de43 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
     "expose-loader": "^0.7.3",
     "extract-text-webpack-plugin": "^4.0.0-beta.0",
     "file-loader": "^1.1.11",
-    "fork-ts-checker-webpack-plugin": "^0.4.1",
+    "fork-ts-checker-webpack-plugin": "^0.4.2",
     "gaze": "^1.1.2",
     "glob": "~7.0.0",
     "grunt": "1.0.1",
@@ -71,12 +71,14 @@
     "karma-webpack": "^3.0.0",
     "lint-staged": "^6.0.0",
     "load-grunt-tasks": "3.5.2",
+    "mini-css-extract-plugin": "^0.4.0",
     "mobx-react-devtools": "^4.2.15",
     "mocha": "^4.0.1",
     "ng-annotate-loader": "^0.6.1",
     "ng-annotate-webpack-plugin": "^0.2.1-pre",
     "ngtemplate-loader": "^2.0.1",
     "npm": "^5.4.2",
+    "optimize-css-assets-webpack-plugin": "^4.0.2",
     "phantomjs-prebuilt": "^2.1.15",
     "postcss-browser-reporter": "^0.5.0",
     "postcss-loader": "^2.0.6",
@@ -90,15 +92,16 @@
     "style-loader": "^0.21.0",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
-    "ts-loader": "^4.3.0",
     "ts-jest": "^22.4.6",
+    "ts-loader": "^4.3.0",
+    "tslib": "^1.9.3",
     "tslint": "^5.8.0",
     "tslint-loader": "^3.5.3",
     "typescript": "^2.6.2",
+    "uglifyjs-webpack-plugin": "^1.2.7",
     "webpack": "^4.8.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-cleanup-plugin": "^0.5.1",
-    "fork-ts-checker-webpack-plugin": "^0.4.2",
     "webpack-cli": "^2.1.4",
     "webpack-dev-server": "^3.1.0",
     "webpack-merge": "^4.1.0",
@@ -155,14 +158,12 @@
     "immutable": "^3.8.2",
     "jquery": "^3.2.1",
     "lodash": "^4.17.10",
-    "mini-css-extract-plugin": "^0.4.0",
     "mobx": "^3.4.1",
     "mobx-react": "^4.3.5",
     "mobx-state-tree": "^1.3.1",
     "moment": "^2.22.2",
     "mousetrap": "^1.6.0",
     "mousetrap-global-bind": "^1.1.0",
-    "optimize-css-assets-webpack-plugin": "^4.0.2",
     "prismjs": "^1.6.0",
     "prop-types": "^15.6.0",
     "react": "^16.2.0",
@@ -181,10 +182,9 @@
     "slate-react": "^0.12.4",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
-    "tinycolor2": "^1.4.1",
-    "uglifyjs-webpack-plugin": "^1.2.7"
+    "tinycolor2": "^1.4.1"
   },
   "resolutions": {
     "caniuse-db": "1.0.30000772"
   }
-}
+}
\ No newline at end of file
diff --git a/scripts/webpack/webpack.hot.js b/scripts/webpack/webpack.hot.js
index 28c8cec504d..0305a6f465c 100644
--- a/scripts/webpack/webpack.hot.js
+++ b/scripts/webpack/webpack.hot.js
@@ -20,6 +20,7 @@ module.exports = merge(common, {
     path: path.resolve(__dirname, '../../public/build'),
     filename: '[name].[hash].js',
     publicPath: "/public/build/",
+    pathinfo: false,
   },
 
   resolve: {
@@ -37,6 +38,12 @@ module.exports = merge(common, {
     }
   },
 
+  optimization: {
+    removeAvailableModules: false,
+    removeEmptyChunks: false,
+    splitChunks: false,
+  },
+
   module: {
     rules: [
       {
@@ -56,7 +63,8 @@ module.exports = merge(common, {
         {
           loader: 'ts-loader',
           options: {
-            transpileOnly: true
+            transpileOnly: true,
+            experimentalWatchApi: true
           },
         }],
       },
diff --git a/tsconfig.json b/tsconfig.json
index 3596930a62f..3ef1dd1b769 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,32 +1,43 @@
 {
-    "compilerOptions": {
-      "moduleResolution": "node",
-      "outDir": "public/dist",
-      "target": "es5",
-      "lib": ["es6", "dom"],
-      "rootDir": "public/",
-      "jsx": "react",
-      "module": "esnext",
-      "declaration": false,
-      "allowSyntheticDefaultImports": true,
-      "inlineSourceMap": false,
-      "sourceMap": true,
-      "noEmitOnError": false,
-      "emitDecoratorMetadata": false,
-      "experimentalDecorators": true,
-      "noImplicitReturns": true,
-      "noImplicitThis": false,
-      "noImplicitUseStrict":false,
-      "noImplicitAny": false,
-      "noUnusedLocals": true,
-      "baseUrl": "public",
-      "paths": {
-        "app": ["app"]
-      }
-    },
-    "include": [
-      "public/app/**/*.ts",
-      "public/app/**/*.tsx",
-      "public/test/**/*.ts"
-    ]
-}
+  "compilerOptions": {
+    "moduleResolution": "node",
+    "outDir": "public/dist",
+    "target": "es5",
+    "lib": [
+      "es6",
+      "dom"
+    ],
+    "rootDir": "public/",
+    "jsx": "react",
+    "module": "esnext",
+    "declaration": false,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true,
+    "importHelpers": true, // importing helper functions from tslib
+    "noEmitHelpers": true, // disable emitting inline helper functions
+    "removeComments": false, // comments are needed by angular injections
+    "inlineSourceMap": false,
+    "sourceMap": true,
+    "noEmitOnError": false,
+    "emitDecoratorMetadata": false,
+    "experimentalDecorators": true,
+    "noImplicitReturns": true,
+    "noImplicitThis": false,
+    "noImplicitUseStrict": false,
+    "noImplicitAny": false,
+    "noUnusedLocals": true,
+    "baseUrl": "public",
+    "pretty": true,
+    "paths": {
+      "app": [
+        "app"
+      ]
+    }
+  },
+  "include": [
+    "public/app/**/*.ts",
+    "public/app/**/*.tsx",
+    "public/test/**/*.ts"
+  ]
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 6772d7c14a4..6e737e33348 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3101,7 +3101,7 @@ d3-request@1.0.6:
     d3-dsv "1"
     xmlhttprequest "1"
 
-d3-scale-chromatic@^1.1.1:
+d3-scale-chromatic@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.0.tgz#7ee38ffcaa7ad55cfed83a6a668aac5570c653c4"
   dependencies:
@@ -7974,7 +7974,7 @@ mocha@^4.0.1:
     mkdirp "0.5.1"
     supports-color "4.4.0"
 
-moment@^2.18.1:
+moment@^2.22.2:
   version "2.22.2"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
 
@@ -12029,6 +12029,10 @@ tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
   version "1.9.2"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.2.tgz#8be0cc9a1f6dc7727c38deb16c2ebd1a2892988e"
 
+tslib@^1.9.3:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
+
 tslint-loader@^3.5.3:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.6.0.tgz#12ed4d5ef57d68be25cd12692fb2108b66469d76"

From 931b944cddb879dfbfb44c5da18bfda43d36a0e9 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 25 Jul 2018 17:38:45 +0200
Subject: [PATCH 100/380] Almost all tests passing

---
 .../specs/variable_srv_init.jest.ts           | 42 +++++--------------
 .../app/features/templating/variable_srv.ts   |  1 -
 2 files changed, 10 insertions(+), 33 deletions(-)

diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.jest.ts
index 519adc0a350..eba0ba8cfee 100644
--- a/public/app/features/templating/specs/variable_srv_init.jest.ts
+++ b/public/app/features/templating/specs/variable_srv_init.jest.ts
@@ -1,13 +1,9 @@
-//import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common';
-
 import '../all';
 
 import _ from 'lodash';
-// import helpers from 'test/specs/helpers';
-// import { Emitter } from 'app/core/core';
 import { VariableSrv } from '../variable_srv';
 import $q from 'q';
-// import { model } from 'mobx-state-tree/dist/internal';
+// import { TemplateSrv } from '../template_srv';
 
 describe('VariableSrv init', function() {
   let templateSrv = {
@@ -16,8 +12,9 @@ describe('VariableSrv init', function() {
     },
     variableInitialized: () => {},
     updateTemplateData: () => {},
-    replace: str => str,
+    replace: () => '  /pee$/',
   };
+  // let templateSrv = new TemplateSrv();
   let $injector = <any>{};
   let $rootscope = {
     $on: () => {},
@@ -29,29 +26,8 @@ describe('VariableSrv init', function() {
     dashboard: {},
   };
 
-  //   beforeEach(angularMocks.module('grafana.core'));
-  //   beforeEach(angularMocks.module('grafana.controllers'));
-  //   beforeEach(angularMocks.module('grafana.services'));
-  //   beforeEach(
-  //     angularMocks.module(function($compileProvider) {
-  //       $compileProvider.preAssignBindingsEnabled(true);
-  //     })
-  //   );
-
-  //   beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
-  //   beforeEach(
-  //     angularMocks.inject(($rootScope, $q, $location, $injector) => {
-  //       ctx.$q = $q;
-  //       ctx.$rootScope = $rootScope;
-  //       ctx.$location = $location;
-  //       ctx.variableSrv = $injector.get('variableSrv');
-  //       ctx.$rootScope.$digest();
-  //     })
-  //   );
-
   function describeInitScenario(desc, fn) {
     describe(desc, function() {
-      //   events: new Emitter(),
       var scenario: any = {
         urlParams: {},
         setup: setupFn => {
@@ -81,14 +57,12 @@ describe('VariableSrv init', function() {
         ctx.variableSrv.datasource = ctx.datasource;
         ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
 
-        ctx.variableSrv.$location.search = () => Promise.resolve(scenario.urlParams);
+        ctx.variableSrv.$location.search = () => scenario.urlParams;
         ctx.variableSrv.dashboard = {
           templating: { list: scenario.variables },
-          // events: new Emitter(),
         };
 
         await ctx.variableSrv.init(ctx.variableSrv.dashboard);
-        // ctx.$rootScope.$digest();
 
         scenario.variables = ctx.variableSrv.variables;
       });
@@ -113,6 +87,7 @@ describe('VariableSrv init', function() {
       });
 
       it('should update current value', () => {
+        console.log(type);
         expect(scenario.variables[0].current.value).toBe('new');
         expect(scenario.variables[0].current.text).toBe('new');
       });
@@ -176,6 +151,7 @@ describe('VariableSrv init', function() {
     });
 
     it('should update current value', function() {
+      console.log(ctx.variableSrv.variables[0].options);
       var variable = ctx.variableSrv.variables[0];
       expect(variable.options.length).toBe(2);
     });
@@ -251,14 +227,16 @@ describe('VariableSrv init', function() {
 });
 
 function getVarMockConstructor(variable, model, ctx) {
-  console.log(model.model.type);
+  //   console.log(model.model.type);
   switch (model.model.type) {
     case 'datasource':
-      return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
+      return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv);
     case 'query':
       return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
     case 'interval':
       return new variable(model.model, {}, ctx.templateSrv, ctx.variableSrv);
+    case 'custom':
+      return new variable(model.model, ctx.variableSrv);
     default:
       return new variable(model.model);
   }
diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts
index 9f6522c9b86..8ad3c2845e2 100644
--- a/public/app/features/templating/variable_srv.ts
+++ b/public/app/features/templating/variable_srv.ts
@@ -23,7 +23,6 @@ export class VariableSrv {
 
     // init variables
     for (let variable of this.variables) {
-      console.log(variable);
       variable.initLock = this.$q.defer();
     }
 

From 35cc85bfcc46efdc79cf22b98741a6ea34b93d58 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 26 Jul 2018 09:36:46 +0200
Subject: [PATCH 101/380] All tests passing. Remove Karma test.

---
 .../specs/variable_srv_init.jest.ts           |  31 ++-
 .../specs/variable_srv_init_specs.ts          | 216 ------------------
 2 files changed, 13 insertions(+), 234 deletions(-)
 delete mode 100644 public/app/features/templating/specs/variable_srv_init_specs.ts

diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.jest.ts
index eba0ba8cfee..ea8689f528b 100644
--- a/public/app/features/templating/specs/variable_srv_init.jest.ts
+++ b/public/app/features/templating/specs/variable_srv_init.jest.ts
@@ -3,7 +3,6 @@ import '../all';
 import _ from 'lodash';
 import { VariableSrv } from '../variable_srv';
 import $q from 'q';
-// import { TemplateSrv } from '../template_srv';
 
 describe('VariableSrv init', function() {
   let templateSrv = {
@@ -12,22 +11,21 @@ describe('VariableSrv init', function() {
     },
     variableInitialized: () => {},
     updateTemplateData: () => {},
-    replace: () => '  /pee$/',
+    replace: str =>
+      str.replace(this.regex, match => {
+        return match;
+      }),
   };
-  // let templateSrv = new TemplateSrv();
+
   let $injector = <any>{};
   let $rootscope = {
     $on: () => {},
   };
 
-  let ctx = <any>{
-    datasourceSrv: {},
-    $location: {},
-    dashboard: {},
-  };
+  let ctx = <any>{};
 
   function describeInitScenario(desc, fn) {
-    describe(desc, function() {
+    describe(desc, () => {
       var scenario: any = {
         urlParams: {},
         setup: setupFn => {
@@ -43,7 +41,7 @@ describe('VariableSrv init', function() {
           },
           datasourceSrv: {
             get: () => Promise.resolve(ctx.datasource),
-            getMetricSources: () => Promise.resolve(scenario.metricSources),
+            getMetricSources: () => scenario.metricSources,
           },
           templateSrv,
         };
@@ -87,7 +85,6 @@ describe('VariableSrv init', function() {
       });
 
       it('should update current value', () => {
-        console.log(type);
         expect(scenario.variables[0].current.value).toBe('new');
         expect(scenario.variables[0].current.text).toBe('new');
       });
@@ -150,8 +147,7 @@ describe('VariableSrv init', function() {
       ];
     });
 
-    it('should update current value', function() {
-      console.log(ctx.variableSrv.variables[0].options);
+    it('should update current value', () => {
       var variable = ctx.variableSrv.variables[0];
       expect(variable.options.length).toBe(2);
     });
@@ -175,7 +171,7 @@ describe('VariableSrv init', function() {
       scenario.urlParams['var-apps'] = ['val2', 'val1'];
     });
 
-    it('should update current value', function() {
+    it('should update current value', () => {
       var variable = ctx.variableSrv.variables[0];
       expect(variable.current.value.length).toBe(2);
       expect(variable.current.value[0]).toBe('val2');
@@ -185,7 +181,7 @@ describe('VariableSrv init', function() {
       expect(variable.options[1].selected).toBe(true);
     });
 
-    it('should set options that are not in value to selected false', function() {
+    it('should set options that are not in value to selected false', () => {
       var variable = ctx.variableSrv.variables[0];
       expect(variable.options[2].selected).toBe(false);
     });
@@ -209,7 +205,7 @@ describe('VariableSrv init', function() {
       scenario.urlParams['var-apps'] = ['val2', 'val1'];
     });
 
-    it('should update current value', function() {
+    it('should update current value', () => {
       var variable = ctx.variableSrv.variables[0];
       expect(variable.current.value.length).toBe(2);
       expect(variable.current.value[0]).toBe('val2');
@@ -219,7 +215,7 @@ describe('VariableSrv init', function() {
       expect(variable.options[1].selected).toBe(true);
     });
 
-    it('should set options that are not in value to selected false', function() {
+    it('should set options that are not in value to selected false', () => {
       var variable = ctx.variableSrv.variables[0];
       expect(variable.options[2].selected).toBe(false);
     });
@@ -227,7 +223,6 @@ describe('VariableSrv init', function() {
 });
 
 function getVarMockConstructor(variable, model, ctx) {
-  //   console.log(model.model.type);
   switch (model.model.type) {
     case 'datasource':
       return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv);
diff --git a/public/app/features/templating/specs/variable_srv_init_specs.ts b/public/app/features/templating/specs/variable_srv_init_specs.ts
deleted file mode 100644
index 11639c6aa8f..00000000000
--- a/public/app/features/templating/specs/variable_srv_init_specs.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common';
-
-import '../all';
-
-import _ from 'lodash';
-import helpers from 'test/specs/helpers';
-import { Emitter } from 'app/core/core';
-
-describe('VariableSrv init', function() {
-  var ctx = new helpers.ControllerTestContext();
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
-
-  beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
-  beforeEach(
-    angularMocks.inject(($rootScope, $q, $location, $injector) => {
-      ctx.$q = $q;
-      ctx.$rootScope = $rootScope;
-      ctx.$location = $location;
-      ctx.variableSrv = $injector.get('variableSrv');
-      ctx.$rootScope.$digest();
-    })
-  );
-
-  function describeInitScenario(desc, fn) {
-    describe(desc, function() {
-      var scenario: any = {
-        urlParams: {},
-        setup: setupFn => {
-          scenario.setupFn = setupFn;
-        },
-      };
-
-      beforeEach(function() {
-        scenario.setupFn();
-        ctx.datasource = {};
-        ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
-
-        ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ctx.datasource));
-        ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
-
-        ctx.$location.search = sinon.stub().returns(scenario.urlParams);
-        ctx.dashboard = {
-          templating: { list: scenario.variables },
-          events: new Emitter(),
-        };
-
-        ctx.variableSrv.init(ctx.dashboard);
-        ctx.$rootScope.$digest();
-
-        scenario.variables = ctx.variableSrv.variables;
-      });
-
-      fn(scenario);
-    });
-  }
-
-  ['query', 'interval', 'custom', 'datasource'].forEach(type => {
-    describeInitScenario('when setting ' + type + ' variable via url', scenario => {
-      scenario.setup(() => {
-        scenario.variables = [
-          {
-            name: 'apps',
-            type: type,
-            current: { text: 'test', value: 'test' },
-            options: [{ text: 'test', value: 'test' }],
-          },
-        ];
-        scenario.urlParams['var-apps'] = 'new';
-        scenario.metricSources = [];
-      });
-
-      it('should update current value', () => {
-        expect(scenario.variables[0].current.value).to.be('new');
-        expect(scenario.variables[0].current.text).to.be('new');
-      });
-    });
-  });
-
-  describe('given dependent variables', () => {
-    var variableList = [
-      {
-        name: 'app',
-        type: 'query',
-        query: '',
-        current: { text: 'app1', value: 'app1' },
-        options: [{ text: 'app1', value: 'app1' }],
-      },
-      {
-        name: 'server',
-        type: 'query',
-        refresh: 1,
-        query: '$app.*',
-        current: { text: 'server1', value: 'server1' },
-        options: [{ text: 'server1', value: 'server1' }],
-      },
-    ];
-
-    describeInitScenario('when setting parent var from url', scenario => {
-      scenario.setup(() => {
-        scenario.variables = _.cloneDeep(variableList);
-        scenario.urlParams['var-app'] = 'google';
-        scenario.queryResult = [{ text: 'google-server1' }, { text: 'google-server2' }];
-      });
-
-      it('should update child variable', () => {
-        expect(scenario.variables[1].options.length).to.be(2);
-        expect(scenario.variables[1].current.text).to.be('google-server1');
-      });
-
-      it('should only update it once', () => {
-        expect(ctx.datasource.metricFindQuery.callCount).to.be(1);
-      });
-    });
-  });
-
-  describeInitScenario('when datasource variable is initialized', scenario => {
-    scenario.setup(() => {
-      scenario.variables = [
-        {
-          type: 'datasource',
-          query: 'graphite',
-          name: 'test',
-          current: { value: 'backend4_pee', text: 'backend4_pee' },
-          regex: '/pee$/',
-        },
-      ];
-      scenario.metricSources = [
-        { name: 'backend1', meta: { id: 'influx' } },
-        { name: 'backend2_pee', meta: { id: 'graphite' } },
-        { name: 'backend3', meta: { id: 'graphite' } },
-        { name: 'backend4_pee', meta: { id: 'graphite' } },
-      ];
-    });
-
-    it('should update current value', function() {
-      var variable = ctx.variableSrv.variables[0];
-      expect(variable.options.length).to.be(2);
-    });
-  });
-
-  describeInitScenario('when template variable is present in url multiple times', scenario => {
-    scenario.setup(() => {
-      scenario.variables = [
-        {
-          name: 'apps',
-          type: 'query',
-          multi: true,
-          current: { text: 'val1', value: 'val1' },
-          options: [
-            { text: 'val1', value: 'val1' },
-            { text: 'val2', value: 'val2' },
-            { text: 'val3', value: 'val3', selected: true },
-          ],
-        },
-      ];
-      scenario.urlParams['var-apps'] = ['val2', 'val1'];
-    });
-
-    it('should update current value', function() {
-      var variable = ctx.variableSrv.variables[0];
-      expect(variable.current.value.length).to.be(2);
-      expect(variable.current.value[0]).to.be('val2');
-      expect(variable.current.value[1]).to.be('val1');
-      expect(variable.current.text).to.be('val2 + val1');
-      expect(variable.options[0].selected).to.be(true);
-      expect(variable.options[1].selected).to.be(true);
-    });
-
-    it('should set options that are not in value to selected false', function() {
-      var variable = ctx.variableSrv.variables[0];
-      expect(variable.options[2].selected).to.be(false);
-    });
-  });
-
-  describeInitScenario('when template variable is present in url multiple times using key/values', scenario => {
-    scenario.setup(() => {
-      scenario.variables = [
-        {
-          name: 'apps',
-          type: 'query',
-          multi: true,
-          current: { text: 'Val1', value: 'val1' },
-          options: [
-            { text: 'Val1', value: 'val1' },
-            { text: 'Val2', value: 'val2' },
-            { text: 'Val3', value: 'val3', selected: true },
-          ],
-        },
-      ];
-      scenario.urlParams['var-apps'] = ['val2', 'val1'];
-    });
-
-    it('should update current value', function() {
-      var variable = ctx.variableSrv.variables[0];
-      expect(variable.current.value.length).to.be(2);
-      expect(variable.current.value[0]).to.be('val2');
-      expect(variable.current.value[1]).to.be('val1');
-      expect(variable.current.text).to.be('Val2 + Val1');
-      expect(variable.options[0].selected).to.be(true);
-      expect(variable.options[1].selected).to.be(true);
-    });
-
-    it('should set options that are not in value to selected false', function() {
-      var variable = ctx.variableSrv.variables[0];
-      expect(variable.options[2].selected).to.be(false);
-    });
-  });
-});

From 88e91b3f51fa2c5a66442bfa3322abbfbeebd950 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 26 Jul 2018 10:44:40 +0200
Subject: [PATCH 102/380] Begin conversion

---
 .../panel/singlestat/specs/singlestat.jest.ts | 384 ++++++++++++++++++
 1 file changed, 384 insertions(+)
 create mode 100644 public/app/plugins/panel/singlestat/specs/singlestat.jest.ts

diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
new file mode 100644
index 00000000000..2c945aa6eb2
--- /dev/null
+++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
@@ -0,0 +1,384 @@
+// import { describe, beforeEach, afterEach, it, sinon, expect, angularMocks } from 'test/lib/common';
+
+// import helpers from 'test/specs/helpers';
+import { SingleStatCtrl } from '../module';
+import moment from 'moment';
+
+describe('SingleStatCtrl', function() {
+  let ctx = <any>{};
+  let epoch = 1505826363746;
+  let clock;
+
+  let $scope = {
+    $on: () => {},
+  };
+
+  let $injector = {
+    get: () => {},
+  };
+
+  SingleStatCtrl.prototype.panel = {
+    events: {
+      on: () => {},
+      emit: () => {},
+    },
+  };
+  SingleStatCtrl.prototype.dashboard = {
+    isTimezoneUtc: () => {},
+  };
+
+  function singleStatScenario(desc, func) {
+    describe(desc, function() {
+      ctx.setup = function(setupFunc) {
+        // beforeEach(angularMocks.module('grafana.services'));
+        // beforeEach(angularMocks.module('grafana.controllers'));
+        // beforeEach(
+        //   angularMocks.module(function($compileProvider) {
+        //     $compileProvider.preAssignBindingsEnabled(true);
+        //   })
+        // );
+
+        // beforeEach(ctx.providePhase());
+        // beforeEach(ctx.createPanelController(SingleStatCtrl));
+
+        beforeEach(function() {
+          ctx.ctrl = new SingleStatCtrl($scope, $injector, {});
+          setupFunc();
+          ctx.ctrl.onDataReceived(ctx.data);
+          ctx.data = ctx.ctrl.data;
+        });
+      };
+
+      func(ctx);
+    });
+  }
+
+  singleStatScenario('with defaults', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
+    });
+
+    it('Should use series avg as default main value', function() {
+      expect(ctx.data.value).toBe(15);
+      expect(ctx.data.valueRounded).toBe(15);
+    });
+
+    it('should set formatted falue', function() {
+      expect(ctx.data.valueFormatted).toBe('15');
+    });
+  });
+
+  singleStatScenario('showing serie name instead of value', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
+      ctx.ctrl.panel.valueName = 'name';
+    });
+
+    it('Should use series avg as default main value', function() {
+      expect(ctx.data.value).toBe(0);
+      expect(ctx.data.valueRounded).toBe(0);
+    });
+
+    it('should set formatted value', function() {
+      expect(ctx.data.valueFormatted).toBe('test.cpu1');
+    });
+  });
+
+  singleStatScenario('showing last iso time instead of value', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.ctrl.panel.valueName = 'last_time';
+      ctx.ctrl.panel.format = 'dateTimeAsIso';
+    });
+
+    it('Should use time instead of value', function() {
+      console.log(ctx.data.value);
+      expect(ctx.data.value).toBe(1505634997920);
+      expect(ctx.data.valueRounded).toBe(1505634997920);
+    });
+
+    it('should set formatted value', function() {
+      expect(ctx.data.valueFormatted).toBe(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss'));
+    });
+  });
+
+  singleStatScenario('showing last iso time instead of value (in UTC)', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.ctrl.panel.valueName = 'last_time';
+      ctx.ctrl.panel.format = 'dateTimeAsIso';
+      //   ctx.setIsUtc(true);
+    });
+
+    it('should set formatted value', function() {
+      expect(ctx.data.valueFormatted).toBe(moment.utc(1505634997920).format('YYYY-MM-DD HH:mm:ss'));
+    });
+  });
+
+  singleStatScenario('showing last us time instead of value', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.ctrl.panel.valueName = 'last_time';
+      ctx.ctrl.panel.format = 'dateTimeAsUS';
+    });
+
+    it('Should use time instead of value', function() {
+      expect(ctx.data.value).toBe(1505634997920);
+      expect(ctx.data.valueRounded).toBe(1505634997920);
+    });
+
+    it('should set formatted value', function() {
+      expect(ctx.data.valueFormatted).toBe(moment(1505634997920).format('MM/DD/YYYY h:mm:ss a'));
+    });
+  });
+
+  singleStatScenario('showing last us time instead of value (in UTC)', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.ctrl.panel.valueName = 'last_time';
+      ctx.ctrl.panel.format = 'dateTimeAsUS';
+      //   ctx.setIsUtc(true);
+    });
+
+    it('should set formatted value', function() {
+      expect(ctx.data.valueFormatted).toBe(moment.utc(1505634997920).format('MM/DD/YYYY h:mm:ss a'));
+    });
+  });
+
+  singleStatScenario('showing last time from now instead of value', function(ctx) {
+    beforeEach(() => {
+      //   clock = sinon.useFakeTimers(epoch);
+      jest.useFakeTimers();
+    });
+
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.ctrl.panel.valueName = 'last_time';
+      ctx.ctrl.panel.format = 'dateTimeFromNow';
+    });
+
+    it('Should use time instead of value', function() {
+      expect(ctx.data.value).toBe(1505634997920);
+      expect(ctx.data.valueRounded).toBe(1505634997920);
+    });
+
+    it('should set formatted value', function() {
+      expect(ctx.data.valueFormatted).toBe('2 days ago');
+    });
+
+    afterEach(() => {
+      jest.clearAllTimers();
+    });
+  });
+
+  singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) {
+    beforeEach(() => {
+      //   clock = sinon.useFakeTimers(epoch);
+      jest.useFakeTimers();
+    });
+
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.ctrl.panel.valueName = 'last_time';
+      ctx.ctrl.panel.format = 'dateTimeFromNow';
+      //   ctx.setIsUtc(true);
+    });
+
+    it('should set formatted value', function() {
+      expect(ctx.data.valueFormatted).toBe('2 days ago');
+    });
+
+    afterEach(() => {
+      jest.clearAllTimers();
+    });
+  });
+
+  singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function(
+    ctx
+  ) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }];
+    });
+
+    it('Should be rounded', function() {
+      expect(ctx.data.value).toBe(99.999495);
+      expect(ctx.data.valueRounded).toBe(100);
+    });
+
+    it('should set formatted value', function() {
+      expect(ctx.data.valueFormatted).toBe('100');
+    });
+  });
+
+  singleStatScenario('When value to text mapping is specified', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }];
+      ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
+    });
+
+    it('value should remain', function() {
+      expect(ctx.data.value).toBe(9.9);
+    });
+
+    it('round should be rounded up', function() {
+      expect(ctx.data.valueRounded).toBe(10);
+    });
+
+    it('Should replace value with text', function() {
+      expect(ctx.data.valueFormatted).toBe('OK');
+    });
+  });
+
+  singleStatScenario('When range to text mapping is specified for first range', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[41, 50]] }];
+      ctx.ctrl.panel.mappingType = 2;
+      ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
+    });
+
+    it('Should replace value with text OK', function() {
+      expect(ctx.data.valueFormatted).toBe('OK');
+    });
+  });
+
+  singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) {
+    ctx.setup(function() {
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[65, 75]] }];
+      ctx.ctrl.panel.mappingType = 2;
+      ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
+    });
+
+    it('Should replace value with text NOT OK', function() {
+      expect(ctx.data.valueFormatted).toBe('NOT OK');
+    });
+  });
+
+  describe('When table data', function() {
+    const tableData = [
+      {
+        columns: [{ text: 'Time', type: 'time' }, { text: 'test1' }, { text: 'mean' }, { text: 'test2' }],
+        rows: [[1492759673649, 'ignore1', 15, 'ignore2']],
+        type: 'table',
+      },
+    ];
+
+    singleStatScenario('with default values', function(ctx) {
+      ctx.setup(function() {
+        ctx.data = tableData;
+        ctx.ctrl.panel.tableColumn = 'mean';
+      });
+
+      it('Should use first rows value as default main value', function() {
+        expect(ctx.data.value).toBe(15);
+        expect(ctx.data.valueRounded).toBe(15);
+      });
+
+      it('should set formatted value', function() {
+        expect(ctx.data.valueFormatted).toBe('15');
+      });
+    });
+
+    singleStatScenario('When table data has multiple columns', function(ctx) {
+      ctx.setup(function() {
+        ctx.data = tableData;
+        ctx.ctrl.panel.tableColumn = '';
+      });
+
+      it('Should set column to first column that is not time', function() {
+        expect(ctx.ctrl.panel.tableColumn).toBe('test1');
+      });
+    });
+
+    singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function(
+      ctx
+    ) {
+      ctx.setup(function() {
+        ctx.data = tableData;
+        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2'];
+        ctx.ctrl.panel.tableColumn = 'mean';
+      });
+
+      it('Should be rounded', function() {
+        expect(ctx.data.value).toBe(99.99999);
+        expect(ctx.data.valueRounded).toBe(100);
+      });
+
+      it('should set formatted falue', function() {
+        expect(ctx.data.valueFormatted).toBe('100');
+      });
+    });
+
+    singleStatScenario('When value to text mapping is specified', function(ctx) {
+      ctx.setup(function() {
+        ctx.data = tableData;
+        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2'];
+        ctx.ctrl.panel.tableColumn = 'mean';
+        ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
+      });
+
+      it('value should remain', function() {
+        expect(ctx.data.value).toBe(9.9);
+      });
+
+      it('round should be rounded up', function() {
+        expect(ctx.data.valueRounded).toBe(10);
+      });
+
+      it('Should replace value with text', function() {
+        expect(ctx.data.valueFormatted).toBe('OK');
+      });
+    });
+
+    singleStatScenario('When range to text mapping is specified for first range', function(ctx) {
+      ctx.setup(function() {
+        ctx.data = tableData;
+        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2'];
+        ctx.ctrl.panel.tableColumn = 'mean';
+        ctx.ctrl.panel.mappingType = 2;
+        ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
+      });
+
+      it('Should replace value with text OK', function() {
+        expect(ctx.data.valueFormatted).toBe('OK');
+      });
+    });
+
+    singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) {
+      ctx.setup(function() {
+        ctx.data = tableData;
+        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
+        ctx.ctrl.panel.tableColumn = 'mean';
+        ctx.ctrl.panel.mappingType = 2;
+        ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
+      });
+
+      it('Should replace value with text NOT OK', function() {
+        expect(ctx.data.valueFormatted).toBe('NOT OK');
+      });
+    });
+
+    singleStatScenario('When value is string', function(ctx) {
+      ctx.setup(function() {
+        ctx.data = tableData;
+        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
+        ctx.ctrl.panel.tableColumn = 'test1';
+      });
+
+      it('Should replace value with text NOT OK', function() {
+        expect(ctx.data.valueFormatted).toBe('ignore1');
+      });
+    });
+
+    singleStatScenario('When value is zero', function(ctx) {
+      ctx.setup(function() {
+        ctx.data = tableData;
+        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2'];
+        ctx.ctrl.panel.tableColumn = 'mean';
+      });
+
+      it('Should return zero', function() {
+        expect(ctx.data.value).toBe(0);
+      });
+    });
+  });
+});

From 7699451d9438546e6655975d53deb7bf6314562d Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Thu, 26 Jul 2018 14:04:12 +0200
Subject: [PATCH 103/380] Refactor Explore query field (#12643)

* Refactor Explore query field

- extract typeahead field that only contains logic for the typeahead
  mechanics
- renamed QueryField to PromQueryField, a wrapper around TypeaheadField
  that deals with Prometheus-specific concepts
- PromQueryField creates a promql typeahead by providing the handlers
  for producing suggestions, and for applying suggestions
- The `refresher` promise is needed to trigger a render once an async
  action in the wrapper returns.

This is prep work for a composable query field to be used by Explore, as
well as editors in datasource plugins.

* Added typeahead handling tests

- extracted context-to-suggestion logic to make it testable
- kept DOM-dependent parts in main onTypeahead funtion

* simplified error handling in explore query field

* Refactor query suggestions

- use monaco's suggestion types (roughly), see
  https://github.com/Microsoft/monaco-editor/blob/f6fb545/monaco.d.ts#L4208
- suggest functions and metrics in empty field (ctrl+space)
- copy and expand prometheus function docs from prometheus datasource
  (will be migrated back to the datasource in the future)

* Added prop and state types, removed unused cwrp

* Split up suggestion processing for code readability
---
 .../Explore/PromQueryField.jest.tsx           | 125 ++++
 .../app/containers/Explore/PromQueryField.tsx | 340 +++++++++++
 public/app/containers/Explore/QueryField.tsx  | 553 ++++++++----------
 public/app/containers/Explore/QueryRows.tsx   |   6 +-
 public/app/containers/Explore/Typeahead.tsx   |  61 +-
 .../Explore/slate-plugins/prism/promql.ts     | 417 +++++++++++--
 public/sass/components/_slate_editor.scss     |   1 +
 7 files changed, 1100 insertions(+), 403 deletions(-)
 create mode 100644 public/app/containers/Explore/PromQueryField.jest.tsx
 create mode 100644 public/app/containers/Explore/PromQueryField.tsx

diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx
new file mode 100644
index 00000000000..8d2903cb2c2
--- /dev/null
+++ b/public/app/containers/Explore/PromQueryField.jest.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import Enzyme, { shallow } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+Enzyme.configure({ adapter: new Adapter() });
+
+import PromQueryField from './PromQueryField';
+
+describe('PromQueryField typeahead handling', () => {
+  const defaultProps = {
+    request: () => ({ data: { data: [] } }),
+  };
+
+  it('returns default suggestions on emtpty context', () => {
+    const instance = shallow(<PromQueryField {...defaultProps} />).instance() as PromQueryField;
+    const result = instance.getTypeahead({ text: '', prefix: '', wrapperClasses: [] });
+    expect(result.context).toBeUndefined();
+    expect(result.refresher).toBeUndefined();
+    expect(result.suggestions.length).toEqual(2);
+  });
+
+  describe('range suggestions', () => {
+    it('returns range suggestions in range context', () => {
+      const instance = shallow(<PromQueryField {...defaultProps} />).instance() as PromQueryField;
+      const result = instance.getTypeahead({ text: '1', prefix: '1', wrapperClasses: ['context-range'] });
+      expect(result.context).toBe('context-range');
+      expect(result.refresher).toBeUndefined();
+      expect(result.suggestions).toEqual([
+        {
+          items: [{ label: '1m' }, { label: '5m' }, { label: '10m' }, { label: '30m' }, { label: '1h' }],
+          label: 'Range vector',
+        },
+      ]);
+    });
+  });
+
+  describe('metric suggestions', () => {
+    it('returns metrics suggestions by default', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} metrics={['foo', 'bar']} />
+      ).instance() as PromQueryField;
+      const result = instance.getTypeahead({ text: 'a', prefix: 'a', wrapperClasses: [] });
+      expect(result.context).toBeUndefined();
+      expect(result.refresher).toBeUndefined();
+      expect(result.suggestions.length).toEqual(2);
+    });
+
+    it('returns default suggestions after a binary operator', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} metrics={['foo', 'bar']} />
+      ).instance() as PromQueryField;
+      const result = instance.getTypeahead({ text: '*', prefix: '', wrapperClasses: [] });
+      expect(result.context).toBeUndefined();
+      expect(result.refresher).toBeUndefined();
+      expect(result.suggestions.length).toEqual(2);
+    });
+  });
+
+  describe('label suggestions', () => {
+    it('returns default label suggestions on label context and no metric', () => {
+      const instance = shallow(<PromQueryField {...defaultProps} />).instance() as PromQueryField;
+      const result = instance.getTypeahead({ text: 'j', prefix: 'j', wrapperClasses: ['context-labels'] });
+      expect(result.context).toBe('context-labels');
+      expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
+    });
+
+    it('returns label suggestions on label context and metric', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} labelKeys={{ foo: ['bar'] }} />
+      ).instance() as PromQueryField;
+      const result = instance.getTypeahead({
+        text: 'job',
+        prefix: 'job',
+        wrapperClasses: ['context-labels'],
+        metric: 'foo',
+      });
+      expect(result.context).toBe('context-labels');
+      expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+    });
+
+    it('returns a refresher on label context and unavailable metric', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} labelKeys={{ foo: ['bar'] }} />
+      ).instance() as PromQueryField;
+      const result = instance.getTypeahead({
+        text: 'job',
+        prefix: 'job',
+        wrapperClasses: ['context-labels'],
+        metric: 'xxx',
+      });
+      expect(result.context).toBeUndefined();
+      expect(result.refresher).toBeInstanceOf(Promise);
+      expect(result.suggestions).toEqual([]);
+    });
+
+    it('returns label values on label context when given a metric and a label key', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} labelKeys={{ foo: ['bar'] }} labelValues={{ foo: { bar: ['baz'] } }} />
+      ).instance() as PromQueryField;
+      const result = instance.getTypeahead({
+        text: '=ba',
+        prefix: 'ba',
+        wrapperClasses: ['context-labels'],
+        metric: 'foo',
+        labelKey: 'bar',
+      });
+      expect(result.context).toBe('context-label-values');
+      expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values' }]);
+    });
+
+    it('returns label suggestions on aggregation context and metric', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} labelKeys={{ foo: ['bar'] }} />
+      ).instance() as PromQueryField;
+      const result = instance.getTypeahead({
+        text: 'job',
+        prefix: 'job',
+        wrapperClasses: ['context-aggregation'],
+        metric: 'foo',
+      });
+      expect(result.context).toBe('context-aggregation');
+      expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+    });
+  });
+});
diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
new file mode 100644
index 00000000000..eb8fc25c67f
--- /dev/null
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -0,0 +1,340 @@
+import _ from 'lodash';
+import React from 'react';
+
+// dom also includes Element polyfills
+import { getNextCharacter, getPreviousCousin } from './utils/dom';
+import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
+import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
+import RunnerPlugin from './slate-plugins/runner';
+import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
+
+import TypeaheadField, {
+  Suggestion,
+  SuggestionGroup,
+  TypeaheadInput,
+  TypeaheadFieldState,
+  TypeaheadOutput,
+} from './QueryField';
+
+const EMPTY_METRIC = '';
+const METRIC_MARK = 'metric';
+const PRISM_LANGUAGE = 'promql';
+
+export const wrapLabel = label => ({ label });
+export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
+  suggestion.move = -1;
+  return suggestion;
+};
+
+export function willApplySuggestion(
+  suggestion: string,
+  { typeaheadContext, typeaheadText }: TypeaheadFieldState
+): string {
+  // Modify suggestion based on context
+  switch (typeaheadContext) {
+    case 'context-labels': {
+      const nextChar = getNextCharacter();
+      if (!nextChar || nextChar === '}' || nextChar === ',') {
+        suggestion += '=';
+      }
+      break;
+    }
+
+    case 'context-label-values': {
+      // Always add quotes and remove existing ones instead
+      if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) {
+        suggestion = `"${suggestion}`;
+      }
+      if (getNextCharacter() !== '"') {
+        suggestion = `${suggestion}"`;
+      }
+      break;
+    }
+
+    default:
+  }
+  return suggestion;
+}
+
+interface PromQueryFieldProps {
+  initialQuery?: string | null;
+  labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
+  labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
+  metrics?: string[];
+  onPressEnter?: () => void;
+  onQueryChange?: (value: string) => void;
+  portalPrefix?: string;
+  request?: (url: string) => any;
+}
+
+interface PromQueryFieldState {
+  labelKeys: { [index: string]: string[] }; // metric -> [labelKey,...]
+  labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
+  metrics: string[];
+}
+
+interface PromTypeaheadInput {
+  text: string;
+  prefix: string;
+  wrapperClasses: string[];
+  metric?: string;
+  labelKey?: string;
+}
+
+class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryFieldState> {
+  plugins: any[];
+
+  constructor(props, context) {
+    super(props, context);
+
+    this.plugins = [
+      RunnerPlugin({ handler: props.onPressEnter }),
+      PluginPrism({ definition: PrismPromql, language: PRISM_LANGUAGE }),
+    ];
+
+    this.state = {
+      labelKeys: props.labelKeys || {},
+      labelValues: props.labelValues || {},
+      metrics: props.metrics || [],
+    };
+  }
+
+  componentDidMount() {
+    this.fetchMetricNames();
+  }
+
+  onChangeQuery = value => {
+    // Send text change to parent
+    const { onQueryChange } = this.props;
+    if (onQueryChange) {
+      onQueryChange(value);
+    }
+  };
+
+  onReceiveMetrics = () => {
+    if (!this.state.metrics) {
+      return;
+    }
+    setPrismTokens(PRISM_LANGUAGE, METRIC_MARK, this.state.metrics);
+  };
+
+  onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
+    const { editorNode, prefix, text, wrapperNode } = typeahead;
+
+    // Get DOM-dependent context
+    const wrapperClasses = Array.from(wrapperNode.classList);
+    // Take first metric as lucky guess
+    const metricNode = editorNode.querySelector(`.${METRIC_MARK}`);
+    const metric = metricNode && metricNode.textContent;
+    const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name');
+    const labelKey = labelKeyNode && labelKeyNode.textContent;
+
+    const result = this.getTypeahead({ text, prefix, wrapperClasses, metric, labelKey });
+
+    console.log('handleTypeahead', wrapperClasses, text, prefix, result.context);
+
+    return result;
+  };
+
+  // Keep this DOM-free for testing
+  getTypeahead({ prefix, wrapperClasses, metric, text }: PromTypeaheadInput): TypeaheadOutput {
+    // Determine candidates by CSS context
+    if (_.includes(wrapperClasses, 'context-range')) {
+      // Suggestions for metric[|]
+      return this.getRangeTypeahead();
+    } else if (_.includes(wrapperClasses, 'context-labels')) {
+      // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
+      return this.getLabelTypeahead.apply(this, arguments);
+    } else if (metric && _.includes(wrapperClasses, 'context-aggregation')) {
+      return this.getAggregationTypeahead.apply(this, arguments);
+    } else if (
+      // Non-empty but not inside known token unless it's a metric
+      (prefix && !_.includes(wrapperClasses, 'token')) ||
+      prefix === metric ||
+      (prefix === '' && !text.match(/^[)\s]+$/)) || // Empty context or after ')'
+      text.match(/[+\-*/^%]/) // After binary operator
+    ) {
+      return this.getEmptyTypeahead();
+    }
+
+    return {
+      suggestions: [],
+    };
+  }
+
+  getEmptyTypeahead(): TypeaheadOutput {
+    const suggestions: SuggestionGroup[] = [];
+    suggestions.push({
+      prefixMatch: true,
+      label: 'Functions',
+      items: FUNCTIONS.map(setFunctionMove),
+    });
+
+    if (this.state.metrics) {
+      suggestions.push({
+        label: 'Metrics',
+        items: this.state.metrics.map(wrapLabel),
+      });
+    }
+    return { suggestions };
+  }
+
+  getRangeTypeahead(): TypeaheadOutput {
+    return {
+      context: 'context-range',
+      suggestions: [
+        {
+          label: 'Range vector',
+          items: [...RATE_RANGES].map(wrapLabel),
+        },
+      ],
+    };
+  }
+
+  getAggregationTypeahead({ metric }: PromTypeaheadInput): TypeaheadOutput {
+    let refresher: Promise<any> = null;
+    const suggestions: SuggestionGroup[] = [];
+    const labelKeys = this.state.labelKeys[metric];
+    if (labelKeys) {
+      suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
+    } else {
+      refresher = this.fetchMetricLabels(metric);
+    }
+
+    return {
+      refresher,
+      suggestions,
+      context: 'context-aggregation',
+    };
+  }
+
+  getLabelTypeahead({ metric, text, wrapperClasses, labelKey }: PromTypeaheadInput): TypeaheadOutput {
+    let context: string;
+    let refresher: Promise<any> = null;
+    const suggestions: SuggestionGroup[] = [];
+    if (metric) {
+      const labelKeys = this.state.labelKeys[metric];
+      if (labelKeys) {
+        if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
+          // Label values
+          if (labelKey) {
+            const labelValues = this.state.labelValues[metric][labelKey];
+            context = 'context-label-values';
+            suggestions.push({
+              label: 'Label values',
+              items: labelValues.map(wrapLabel),
+            });
+          }
+        } else {
+          // Label keys
+          context = 'context-labels';
+          suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
+        }
+      } else {
+        refresher = this.fetchMetricLabels(metric);
+      }
+    } else {
+      // Metric-independent label queries
+      const defaultKeys = ['job', 'instance'];
+      // Munge all keys that we have seen together
+      const labelKeys = Object.keys(this.state.labelKeys).reduce((acc, metric) => {
+        return acc.concat(this.state.labelKeys[metric].filter(key => acc.indexOf(key) === -1));
+      }, defaultKeys);
+      if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
+        // Label values
+        if (labelKey) {
+          if (this.state.labelValues[EMPTY_METRIC]) {
+            const labelValues = this.state.labelValues[EMPTY_METRIC][labelKey];
+            context = 'context-label-values';
+            suggestions.push({
+              label: 'Label values',
+              items: labelValues.map(wrapLabel),
+            });
+          } else {
+            // Can only query label values for now (API to query keys is under development)
+            refresher = this.fetchLabelValues(labelKey);
+          }
+        }
+      } else {
+        // Label keys
+        context = 'context-labels';
+        suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
+      }
+    }
+    return { context, refresher, suggestions };
+  }
+
+  request = url => {
+    if (this.props.request) {
+      return this.props.request(url);
+    }
+    return fetch(url);
+  };
+
+  async fetchLabelValues(key) {
+    const url = `/api/v1/label/${key}/values`;
+    try {
+      const res = await this.request(url);
+      const body = await (res.data || res.json());
+      const pairs = this.state.labelValues[EMPTY_METRIC];
+      const values = {
+        ...pairs,
+        [key]: body.data,
+      };
+      const labelValues = {
+        ...this.state.labelValues,
+        [EMPTY_METRIC]: values,
+      };
+      this.setState({ labelValues });
+    } catch (e) {
+      console.error(e);
+    }
+  }
+
+  async fetchMetricLabels(name) {
+    const url = `/api/v1/series?match[]=${name}`;
+    try {
+      const res = await this.request(url);
+      const body = await (res.data || res.json());
+      const { keys, values } = processLabels(body.data);
+      const labelKeys = {
+        ...this.state.labelKeys,
+        [name]: keys,
+      };
+      const labelValues = {
+        ...this.state.labelValues,
+        [name]: values,
+      };
+      this.setState({ labelKeys, labelValues });
+    } catch (e) {
+      console.error(e);
+    }
+  }
+
+  async fetchMetricNames() {
+    const url = '/api/v1/label/__name__/values';
+    try {
+      const res = await this.request(url);
+      const body = await (res.data || res.json());
+      this.setState({ metrics: body.data }, this.onReceiveMetrics);
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
+  render() {
+    return (
+      <TypeaheadField
+        additionalPlugins={this.plugins}
+        cleanText={cleanText}
+        initialValue={this.props.initialQuery}
+        onTypeahead={this.onTypeahead}
+        onWillApplySuggestion={willApplySuggestion}
+        onValueChanged={this.onChangeQuery}
+        placeholder="Enter a PromQL query"
+      />
+    );
+  }
+}
+
+export default PromQueryField;
diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx
index 41f6d53541c..60caddcad31 100644
--- a/public/app/containers/Explore/QueryField.tsx
+++ b/public/app/containers/Explore/QueryField.tsx
@@ -1,106 +1,163 @@
+import _ from 'lodash';
 import React from 'react';
 import ReactDOM from 'react-dom';
-import { Value } from 'slate';
+import { Block, Change, Document, Text, Value } from 'slate';
 import { Editor } from 'slate-react';
 import Plain from 'slate-plain-serializer';
 
-// dom also includes Element polyfills
-import { getNextCharacter, getPreviousCousin } from './utils/dom';
 import BracesPlugin from './slate-plugins/braces';
 import ClearPlugin from './slate-plugins/clear';
 import NewlinePlugin from './slate-plugins/newline';
-import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
-import RunnerPlugin from './slate-plugins/runner';
-import debounce from './utils/debounce';
-import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
 
 import Typeahead from './Typeahead';
 
-const EMPTY_METRIC = '';
-const METRIC_MARK = 'metric';
 export const TYPEAHEAD_DEBOUNCE = 300;
 
-function flattenSuggestions(s) {
+function flattenSuggestions(s: any[]): any[] {
   return s ? s.reduce((acc, g) => acc.concat(g.items), []) : [];
 }
 
-export const getInitialValue = query =>
-  Value.fromJSON({
-    document: {
-      nodes: [
-        {
-          object: 'block',
-          type: 'paragraph',
-          nodes: [
-            {
-              object: 'text',
-              leaves: [
-                {
-                  text: query,
-                },
-              ],
-            },
-          ],
-        },
-      ],
-    },
+export const makeFragment = (text: string): Document => {
+  const lines = text.split('\n').map(line =>
+    Block.create({
+      type: 'paragraph',
+      nodes: [Text.create(line)],
+    })
+  );
+
+  const fragment = Document.create({
+    nodes: lines,
   });
+  return fragment;
+};
 
-class Portal extends React.Component<any, any> {
-  node: any;
+export const getInitialValue = (value: string): Value => Value.create({ document: makeFragment(value) });
 
-  constructor(props) {
-    super(props);
-    const { index = 0, prefix = 'query' } = props;
-    this.node = document.createElement('div');
-    this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`);
-    document.body.appendChild(this.node);
-  }
-
-  componentWillUnmount() {
-    document.body.removeChild(this.node);
-  }
-
-  render() {
-    return ReactDOM.createPortal(this.props.children, this.node);
-  }
+export interface Suggestion {
+  /**
+   * The label of this completion item. By default
+   * this is also the text that is inserted when selecting
+   * this completion.
+   */
+  label: string;
+  /**
+   * The kind of this completion item. Based on the kind
+   * an icon is chosen by the editor.
+   */
+  kind?: string;
+  /**
+   * A human-readable string with additional information
+   * about this item, like type or symbol information.
+   */
+  detail?: string;
+  /**
+   * A human-readable string, can be Markdown, that represents a doc-comment.
+   */
+  documentation?: string;
+  /**
+   * A string that should be used when comparing this item
+   * with other items. When `falsy` the `label` is used.
+   */
+  sortText?: string;
+  /**
+   * A string that should be used when filtering a set of
+   * completion items. When `falsy` the `label` is used.
+   */
+  filterText?: string;
+  /**
+   * A string or snippet that should be inserted in a document when selecting
+   * this completion. When `falsy` the `label` is used.
+   */
+  insertText?: string;
+  /**
+   * Delete number of characters before the caret position,
+   * by default the letters from the beginning of the word.
+   */
+  deleteBackwards?: number;
+  /**
+   * Number of steps to move after the insertion, can be negative.
+   */
+  move?: number;
 }
 
-class QueryField extends React.Component<any, any> {
-  menuEl: any;
-  plugins: any;
+export interface SuggestionGroup {
+  /**
+   * Label that will be displayed for all entries of this group.
+   */
+  label: string;
+  /**
+   * List of suggestions of this group.
+   */
+  items: Suggestion[];
+  /**
+   * If true, match only by prefix (and not mid-word).
+   */
+  prefixMatch?: boolean;
+  /**
+   * If true, do not filter items in this group based on the search.
+   */
+  skipFilter?: boolean;
+}
+
+interface TypeaheadFieldProps {
+  additionalPlugins?: any[];
+  cleanText?: (text: string) => string;
+  initialValue: string | null;
+  onBlur?: () => void;
+  onFocus?: () => void;
+  onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
+  onValueChanged?: (value: Value) => void;
+  onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string;
+  placeholder?: string;
+  portalPrefix?: string;
+}
+
+export interface TypeaheadFieldState {
+  suggestions: SuggestionGroup[];
+  typeaheadContext: string | null;
+  typeaheadIndex: number;
+  typeaheadPrefix: string;
+  typeaheadText: string;
+  value: Value;
+}
+
+export interface TypeaheadInput {
+  editorNode: Element;
+  prefix: string;
+  selection?: Selection;
+  text: string;
+  wrapperNode: Element;
+}
+
+export interface TypeaheadOutput {
+  context?: string;
+  refresher?: Promise<{}>;
+  suggestions: SuggestionGroup[];
+}
+
+class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldState> {
+  menuEl: HTMLElement | null;
+  plugins: any[];
   resetTimer: any;
 
   constructor(props, context) {
     super(props, context);
 
-    const { prismDefinition = {}, prismLanguage = 'promql' } = props;
-
-    this.plugins = [
-      BracesPlugin(),
-      ClearPlugin(),
-      RunnerPlugin({ handler: props.onPressEnter }),
-      NewlinePlugin(),
-      PluginPrism({ definition: prismDefinition, language: prismLanguage }),
-    ];
+    // Base plugins
+    this.plugins = [BracesPlugin(), ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins];
 
     this.state = {
-      labelKeys: {},
-      labelValues: {},
-      metrics: props.metrics || [],
       suggestions: [],
+      typeaheadContext: null,
       typeaheadIndex: 0,
       typeaheadPrefix: '',
-      value: getInitialValue(props.initialQuery || ''),
+      typeaheadText: '',
+      value: getInitialValue(props.initialValue || ''),
     };
   }
 
   componentDidMount() {
     this.updateMenu();
-
-    if (this.props.metrics === undefined) {
-      this.fetchMetricNames();
-    }
   }
 
   componentWillUnmount() {
@@ -112,12 +169,9 @@ class QueryField extends React.Component<any, any> {
   }
 
   componentWillReceiveProps(nextProps) {
-    if (nextProps.metrics && nextProps.metrics !== this.props.metrics) {
-      this.setState({ metrics: nextProps.metrics }, this.onMetricsReceived);
-    }
-    // initialQuery is null in case the user typed
-    if (nextProps.initialQuery !== null && nextProps.initialQuery !== this.props.initialQuery) {
-      this.setState({ value: getInitialValue(nextProps.initialQuery) });
+    // initialValue is null in case the user typed
+    if (nextProps.initialValue !== null && nextProps.initialValue !== this.props.initialValue) {
+      this.setState({ value: getInitialValue(nextProps.initialValue) });
     }
   }
 
@@ -125,48 +179,28 @@ class QueryField extends React.Component<any, any> {
     const changed = value.document !== this.state.value.document;
     this.setState({ value }, () => {
       if (changed) {
-        this.handleChangeQuery();
+        this.handleChangeValue();
       }
     });
 
-    window.requestAnimationFrame(this.handleTypeahead);
-  };
-
-  onMetricsReceived = () => {
-    if (!this.state.metrics) {
-      return;
+    if (changed) {
+      window.requestAnimationFrame(this.handleTypeahead);
     }
-    setPrismTokens(this.props.prismLanguage, METRIC_MARK, this.state.metrics);
-
-    // Trigger re-render
-    window.requestAnimationFrame(() => {
-      // Bogus edit to trigger highlighting
-      const change = this.state.value
-        .change()
-        .insertText(' ')
-        .deleteBackward(1);
-      this.onChange(change);
-    });
   };
 
-  request = url => {
-    if (this.props.request) {
-      return this.props.request(url);
-    }
-    return fetch(url);
-  };
-
-  handleChangeQuery = () => {
+  handleChangeValue = () => {
     // Send text change to parent
-    const { onQueryChange } = this.props;
-    if (onQueryChange) {
-      onQueryChange(Plain.serialize(this.state.value));
+    const { onValueChanged } = this.props;
+    if (onValueChanged) {
+      onValueChanged(Plain.serialize(this.state.value));
     }
   };
 
-  handleTypeahead = debounce(() => {
+  handleTypeahead = _.debounce(async () => {
     const selection = window.getSelection();
-    if (selection.anchorNode) {
+    const { cleanText, onTypeahead } = this.props;
+
+    if (onTypeahead && selection.anchorNode) {
       const wrapperNode = selection.anchorNode.parentElement;
       const editorNode = wrapperNode.closest('.slate-query-field');
       if (!editorNode || this.state.value.isBlurred) {
@@ -175,164 +209,96 @@ class QueryField extends React.Component<any, any> {
       }
 
       const range = selection.getRangeAt(0);
-      const text = selection.anchorNode.textContent;
       const offset = range.startOffset;
-      const prefix = cleanText(text.substr(0, offset));
-
-      // Determine candidates by context
-      const suggestionGroups = [];
-      const wrapperClasses = wrapperNode.classList;
-      let typeaheadContext = null;
-
-      // Take first metric as lucky guess
-      const metricNode = editorNode.querySelector(`.${METRIC_MARK}`);
-
-      if (wrapperClasses.contains('context-range')) {
-        // Rate ranges
-        typeaheadContext = 'context-range';
-        suggestionGroups.push({
-          label: 'Range vector',
-          items: [...RATE_RANGES],
-        });
-      } else if (wrapperClasses.contains('context-labels') && metricNode) {
-        const metric = metricNode.textContent;
-        const labelKeys = this.state.labelKeys[metric];
-        if (labelKeys) {
-          if ((text && text.startsWith('=')) || wrapperClasses.contains('attr-value')) {
-            // Label values
-            const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name');
-            if (labelKeyNode) {
-              const labelKey = labelKeyNode.textContent;
-              const labelValues = this.state.labelValues[metric][labelKey];
-              typeaheadContext = 'context-label-values';
-              suggestionGroups.push({
-                label: 'Label values',
-                items: labelValues,
-              });
-            }
-          } else {
-            // Label keys
-            typeaheadContext = 'context-labels';
-            suggestionGroups.push({ label: 'Labels', items: labelKeys });
-          }
-        } else {
-          this.fetchMetricLabels(metric);
-        }
-      } else if (wrapperClasses.contains('context-labels') && !metricNode) {
-        // Empty name queries
-        const defaultKeys = ['job', 'instance'];
-        // Munge all keys that we have seen together
-        const labelKeys = Object.keys(this.state.labelKeys).reduce((acc, metric) => {
-          return acc.concat(this.state.labelKeys[metric].filter(key => acc.indexOf(key) === -1));
-        }, defaultKeys);
-        if ((text && text.startsWith('=')) || wrapperClasses.contains('attr-value')) {
-          // Label values
-          const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name');
-          if (labelKeyNode) {
-            const labelKey = labelKeyNode.textContent;
-            if (this.state.labelValues[EMPTY_METRIC]) {
-              const labelValues = this.state.labelValues[EMPTY_METRIC][labelKey];
-              typeaheadContext = 'context-label-values';
-              suggestionGroups.push({
-                label: 'Label values',
-                items: labelValues,
-              });
-            } else {
-              // Can only query label values for now (API to query keys is under development)
-              this.fetchLabelValues(labelKey);
-            }
-          }
-        } else {
-          // Label keys
-          typeaheadContext = 'context-labels';
-          suggestionGroups.push({ label: 'Labels', items: labelKeys });
-        }
-      } else if (metricNode && wrapperClasses.contains('context-aggregation')) {
-        typeaheadContext = 'context-aggregation';
-        const metric = metricNode.textContent;
-        const labelKeys = this.state.labelKeys[metric];
-        if (labelKeys) {
-          suggestionGroups.push({ label: 'Labels', items: labelKeys });
-        } else {
-          this.fetchMetricLabels(metric);
-        }
-      } else if (
-        (this.state.metrics && ((prefix && !wrapperClasses.contains('token')) || text.match(/[+\-*/^%]/))) ||
-        wrapperClasses.contains('context-function')
-      ) {
-        // Need prefix for metrics
-        typeaheadContext = 'context-metrics';
-        suggestionGroups.push({
-          label: 'Metrics',
-          items: this.state.metrics,
-        });
+      const text = selection.anchorNode.textContent;
+      let prefix = text.substr(0, offset);
+      if (cleanText) {
+        prefix = cleanText(prefix);
       }
 
-      let results = 0;
-      const filteredSuggestions = suggestionGroups.map(group => {
-        if (group.items) {
-          group.items = group.items.filter(c => c.length !== prefix.length && c.indexOf(prefix) > -1);
-          results += group.items.length;
+      const { suggestions, context, refresher } = onTypeahead({
+        editorNode,
+        prefix,
+        selection,
+        text,
+        wrapperNode,
+      });
+
+      const filteredSuggestions = suggestions
+        .map(group => {
+          if (group.items) {
+            if (prefix) {
+              // Filter groups based on prefix
+              if (!group.skipFilter) {
+                group.items = group.items.filter(c => (c.filterText || c.label).length >= prefix.length);
+                if (group.prefixMatch) {
+                  group.items = group.items.filter(c => (c.filterText || c.label).indexOf(prefix) === 0);
+                } else {
+                  group.items = group.items.filter(c => (c.filterText || c.label).indexOf(prefix) > -1);
+                }
+              }
+              // Filter out the already typed value (prefix) unless it inserts custom text
+              group.items = group.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix);
+            }
+
+            group.items = _.sortBy(group.items, item => item.sortText || item.label);
+          }
+          return group;
+        })
+        .filter(group => group.items && group.items.length > 0); // Filter out empty groups
+
+      this.setState(
+        {
+          suggestions: filteredSuggestions,
+          typeaheadPrefix: prefix,
+          typeaheadContext: context,
+          typeaheadText: text,
+        },
+        () => {
+          if (refresher) {
+            refresher.then(this.handleTypeahead).catch(e => console.error(e));
+          }
         }
-        return group;
-      });
-
-      console.log('handleTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext);
-
-      this.setState({
-        typeaheadPrefix: prefix,
-        typeaheadContext,
-        typeaheadText: text,
-        suggestions: results > 0 ? filteredSuggestions : [],
-      });
+      );
     }
   }, TYPEAHEAD_DEBOUNCE);
 
-  applyTypeahead(change, suggestion) {
-    const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state;
+  applyTypeahead(change: Change, suggestion: Suggestion): Change {
+    const { cleanText, onWillApplySuggestion } = this.props;
+    const { typeaheadPrefix, typeaheadText } = this.state;
+    let suggestionText = suggestion.insertText || suggestion.label;
+    const move = suggestion.move || 0;
 
-    // Modify suggestion based on context
-    switch (typeaheadContext) {
-      case 'context-labels': {
-        const nextChar = getNextCharacter();
-        if (!nextChar || nextChar === '}' || nextChar === ',') {
-          suggestion += '=';
-        }
-        break;
-      }
-
-      case 'context-label-values': {
-        // Always add quotes and remove existing ones instead
-        if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) {
-          suggestion = `"${suggestion}`;
-        }
-        if (getNextCharacter() !== '"') {
-          suggestion = `${suggestion}"`;
-        }
-        break;
-      }
-
-      default:
+    if (onWillApplySuggestion) {
+      suggestionText = onWillApplySuggestion(suggestionText, { ...this.state });
     }
 
     this.resetTypeahead();
 
     // Remove the current, incomplete text and replace it with the selected suggestion
-    let backward = typeaheadPrefix.length;
-    const text = cleanText(typeaheadText);
+    const backward = suggestion.deleteBackwards || typeaheadPrefix.length;
+    const text = cleanText ? cleanText(typeaheadText) : typeaheadText;
     const suffixLength = text.length - typeaheadPrefix.length;
     const offset = typeaheadText.indexOf(typeaheadPrefix);
-    const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestion === typeaheadText);
+    const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
     const forward = midWord ? suffixLength + offset : 0;
 
-    return (
-      change
-        // TODO this line breaks if cursor was moved left and length is longer than whole prefix
+    // If new-lines, apply suggestion as block
+    if (suggestionText.match(/\n/)) {
+      const fragment = makeFragment(suggestionText);
+      return change
         .deleteBackward(backward)
         .deleteForward(forward)
-        .insertText(suggestion)
-        .focus()
-    );
+        .insertFragment(fragment)
+        .focus();
+    }
+
+    return change
+      .deleteBackward(backward)
+      .deleteForward(forward)
+      .insertText(suggestionText)
+      .move(move)
+      .focus();
   }
 
   onKeyDown = (event, change) => {
@@ -413,74 +379,6 @@ class QueryField extends React.Component<any, any> {
     });
   };
 
-  async fetchLabelValues(key) {
-    const url = `/api/v1/label/${key}/values`;
-    try {
-      const res = await this.request(url);
-      console.log(res);
-      const body = await (res.data || res.json());
-      const pairs = this.state.labelValues[EMPTY_METRIC];
-      const values = {
-        ...pairs,
-        [key]: body.data,
-      };
-      // const labelKeys = {
-      //   ...this.state.labelKeys,
-      //   [EMPTY_METRIC]: keys,
-      // };
-      const labelValues = {
-        ...this.state.labelValues,
-        [EMPTY_METRIC]: values,
-      };
-      this.setState({ labelValues }, this.handleTypeahead);
-    } catch (e) {
-      if (this.props.onRequestError) {
-        this.props.onRequestError(e);
-      } else {
-        console.error(e);
-      }
-    }
-  }
-
-  async fetchMetricLabels(name) {
-    const url = `/api/v1/series?match[]=${name}`;
-    try {
-      const res = await this.request(url);
-      const body = await (res.data || res.json());
-      const { keys, values } = processLabels(body.data);
-      const labelKeys = {
-        ...this.state.labelKeys,
-        [name]: keys,
-      };
-      const labelValues = {
-        ...this.state.labelValues,
-        [name]: values,
-      };
-      this.setState({ labelKeys, labelValues }, this.handleTypeahead);
-    } catch (e) {
-      if (this.props.onRequestError) {
-        this.props.onRequestError(e);
-      } else {
-        console.error(e);
-      }
-    }
-  }
-
-  async fetchMetricNames() {
-    const url = '/api/v1/label/__name__/values';
-    try {
-      const res = await this.request(url);
-      const body = await (res.data || res.json());
-      this.setState({ metrics: body.data }, this.onMetricsReceived);
-    } catch (error) {
-      if (this.props.onRequestError) {
-        this.props.onRequestError(error);
-      } else {
-        console.error(error);
-      }
-    }
-  }
-
   handleBlur = () => {
     const { onBlur } = this.props;
     // If we dont wait here, menu clicks wont work because the menu
@@ -498,7 +396,7 @@ class QueryField extends React.Component<any, any> {
     }
   };
 
-  handleClickMenu = item => {
+  onClickMenu = (item: Suggestion) => {
     // Manually triggering change
     const change = this.applyTypeahead(this.state.value.change(), item);
     this.onChange(change);
@@ -531,7 +429,7 @@ class QueryField extends React.Component<any, any> {
 
       // Write DOM
       requestAnimationFrame(() => {
-        menu.style.opacity = 1;
+        menu.style.opacity = '1';
         menu.style.top = `${rect.top + scrollY + rect.height + 4}px`;
         menu.style.left = `${rect.left + scrollX - 2}px`;
       });
@@ -554,17 +452,16 @@ class QueryField extends React.Component<any, any> {
     let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
     const flattenedSuggestions = flattenSuggestions(suggestions);
     selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
-    const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(
-      i => (typeof i === 'object' ? i.text : i)
-    );
+    const selectedItem: Suggestion | null =
+      flattenedSuggestions.length > 0 ? flattenedSuggestions[selectedIndex] : null;
 
     // Create typeahead in DOM root so we can later position it absolutely
     return (
       <Portal prefix={portalPrefix}>
         <Typeahead
           menuRef={this.menuRef}
-          selectedItems={selectedKeys}
-          onClickItem={this.handleClickMenu}
+          selectedItem={selectedItem}
+          onClickItem={this.onClickMenu}
           groupedItems={suggestions}
         />
       </Portal>
@@ -591,4 +488,24 @@ class QueryField extends React.Component<any, any> {
   }
 }
 
+class Portal extends React.Component<{ index?: number; prefix: string }, {}> {
+  node: HTMLElement;
+
+  constructor(props) {
+    super(props);
+    const { index = 0, prefix = 'query' } = props;
+    this.node = document.createElement('div');
+    this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`);
+    document.body.appendChild(this.node);
+  }
+
+  componentWillUnmount() {
+    document.body.removeChild(this.node);
+  }
+
+  render() {
+    return ReactDOM.createPortal(this.props.children, this.node);
+  }
+}
+
 export default QueryField;
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx
index a968e1e2c64..3aaa006d6df 100644
--- a/public/app/containers/Explore/QueryRows.tsx
+++ b/public/app/containers/Explore/QueryRows.tsx
@@ -1,7 +1,6 @@
 import React, { PureComponent } from 'react';
 
-import promql from './slate-plugins/prism/promql';
-import QueryField from './QueryField';
+import QueryField from './PromQueryField';
 
 class QueryRow extends PureComponent<any, any> {
   constructor(props) {
@@ -62,9 +61,6 @@ class QueryRow extends PureComponent<any, any> {
             portalPrefix="explore"
             onPressEnter={this.handlePressEnter}
             onQueryChange={this.handleChangeQuery}
-            placeholder="Enter a PromQL query"
-            prismLanguage="promql"
-            prismDefinition={promql}
             request={request}
           />
         </div>
diff --git a/public/app/containers/Explore/Typeahead.tsx b/public/app/containers/Explore/Typeahead.tsx
index 44fce7f8c7e..9924488035c 100644
--- a/public/app/containers/Explore/Typeahead.tsx
+++ b/public/app/containers/Explore/Typeahead.tsx
@@ -1,17 +1,26 @@
 import React from 'react';
 
-function scrollIntoView(el) {
+import { Suggestion, SuggestionGroup } from './QueryField';
+
+function scrollIntoView(el: HTMLElement) {
   if (!el || !el.offsetParent) {
     return;
   }
-  const container = el.offsetParent;
+  const container = el.offsetParent as HTMLElement;
   if (el.offsetTop > container.scrollTop + container.offsetHeight || el.offsetTop < container.scrollTop) {
     container.scrollTop = el.offsetTop - container.offsetTop;
   }
 }
 
-class TypeaheadItem extends React.PureComponent<any, any> {
-  el: any;
+interface TypeaheadItemProps {
+  isSelected: boolean;
+  item: Suggestion;
+  onClickItem: (Suggestion) => void;
+}
+
+class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
+  el: HTMLElement;
+
   componentDidUpdate(prevProps) {
     if (this.props.isSelected && !prevProps.isSelected) {
       scrollIntoView(this.el);
@@ -22,20 +31,30 @@ class TypeaheadItem extends React.PureComponent<any, any> {
     this.el = el;
   };
 
+  onClick = () => {
+    this.props.onClickItem(this.props.item);
+  };
+
   render() {
-    const { hint, isSelected, label, onClickItem } = this.props;
+    const { isSelected, item } = this.props;
     const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
-    const onClick = () => onClickItem(label);
     return (
-      <li ref={this.getRef} className={className} onClick={onClick}>
-        {label}
-        {hint && isSelected ? <div className="typeahead-item-hint">{hint}</div> : null}
+      <li ref={this.getRef} className={className} onClick={this.onClick}>
+        {item.detail || item.label}
+        {item.documentation && isSelected ? <div className="typeahead-item-hint">{item.documentation}</div> : null}
       </li>
     );
   }
 }
 
-class TypeaheadGroup extends React.PureComponent<any, any> {
+interface TypeaheadGroupProps {
+  items: Suggestion[];
+  label: string;
+  onClickItem: (Suggestion) => void;
+  selected: Suggestion;
+}
+
+class TypeaheadGroup extends React.PureComponent<TypeaheadGroupProps, {}> {
   render() {
     const { items, label, selected, onClickItem } = this.props;
     return (
@@ -43,16 +62,8 @@ class TypeaheadGroup extends React.PureComponent<any, any> {
         <div className="typeahead-group__title">{label}</div>
         <ul className="typeahead-group__list">
           {items.map(item => {
-            const text = typeof item === 'object' ? item.text : item;
-            const label = typeof item === 'object' ? item.display || item.text : item;
             return (
-              <TypeaheadItem
-                key={text}
-                onClickItem={onClickItem}
-                isSelected={selected.indexOf(text) > -1}
-                hint={item.hint}
-                label={label}
-              />
+              <TypeaheadItem key={item.label} onClickItem={onClickItem} isSelected={selected === item} item={item} />
             );
           })}
         </ul>
@@ -61,13 +72,19 @@ class TypeaheadGroup extends React.PureComponent<any, any> {
   }
 }
 
-class Typeahead extends React.PureComponent<any, any> {
+interface TypeaheadProps {
+  groupedItems: SuggestionGroup[];
+  menuRef: any;
+  selectedItem: Suggestion | null;
+  onClickItem: (Suggestion) => void;
+}
+class Typeahead extends React.PureComponent<TypeaheadProps, {}> {
   render() {
-    const { groupedItems, menuRef, selectedItems, onClickItem } = this.props;
+    const { groupedItems, menuRef, selectedItem, onClickItem } = this.props;
     return (
       <ul className="typeahead" ref={menuRef}>
         {groupedItems.map(g => (
-          <TypeaheadGroup key={g.label} onClickItem={onClickItem} selected={selectedItems} {...g} />
+          <TypeaheadGroup key={g.label} onClickItem={onClickItem} selected={selectedItem} {...g} />
         ))}
       </ul>
     );
diff --git a/public/app/containers/Explore/slate-plugins/prism/promql.ts b/public/app/containers/Explore/slate-plugins/prism/promql.ts
index 0f0be18cb6f..a17c5fbc4f6 100644
--- a/public/app/containers/Explore/slate-plugins/prism/promql.ts
+++ b/public/app/containers/Explore/slate-plugins/prism/promql.ts
@@ -1,67 +1,368 @@
+/* tslint:disable max-line-length */
+
 export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without'];
 
 const AGGREGATION_OPERATORS = [
-  'sum',
-  'min',
-  'max',
-  'avg',
-  'stddev',
-  'stdvar',
-  'count',
-  'count_values',
-  'bottomk',
-  'topk',
-  'quantile',
+  {
+    label: 'sum',
+    insertText: 'sum()',
+    documentation: 'Calculate sum over dimensions',
+  },
+  {
+    label: 'min',
+    insertText: 'min()',
+    documentation: 'Select minimum over dimensions',
+  },
+  {
+    label: 'max',
+    insertText: 'max()',
+    documentation: 'Select maximum over dimensions',
+  },
+  {
+    label: 'avg',
+    insertText: 'avg()',
+    documentation: 'Calculate the average over dimensions',
+  },
+  {
+    label: 'stddev',
+    insertText: 'stddev()',
+    documentation: 'Calculate population standard deviation over dimensions',
+  },
+  {
+    label: 'stdvar',
+    insertText: 'stdvar()',
+    documentation: 'Calculate population standard variance over dimensions',
+  },
+  {
+    label: 'count',
+    insertText: 'count()',
+    documentation: 'Count number of elements in the vector',
+  },
+  {
+    label: 'count_values',
+    insertText: 'count_values()',
+    documentation: 'Count number of elements with the same value',
+  },
+  {
+    label: 'bottomk',
+    insertText: 'bottomk()',
+    documentation: 'Smallest k elements by sample value',
+  },
+  {
+    label: 'topk',
+    insertText: 'topk()',
+    documentation: 'Largest k elements by sample value',
+  },
+  {
+    label: 'quantile',
+    insertText: 'quantile()',
+    documentation: 'Calculate φ-quantile (0 ≤ φ ≤ 1) over dimensions',
+  },
 ];
 
 export const FUNCTIONS = [
   ...AGGREGATION_OPERATORS,
-  'abs',
-  'absent',
-  'ceil',
-  'changes',
-  'clamp_max',
-  'clamp_min',
-  'count_scalar',
-  'day_of_month',
-  'day_of_week',
-  'days_in_month',
-  'delta',
-  'deriv',
-  'drop_common_labels',
-  'exp',
-  'floor',
-  'histogram_quantile',
-  'holt_winters',
-  'hour',
-  'idelta',
-  'increase',
-  'irate',
-  'label_replace',
-  'ln',
-  'log2',
-  'log10',
-  'minute',
-  'month',
-  'predict_linear',
-  'rate',
-  'resets',
-  'round',
-  'scalar',
-  'sort',
-  'sort_desc',
-  'sqrt',
-  'time',
-  'vector',
-  'year',
-  'avg_over_time',
-  'min_over_time',
-  'max_over_time',
-  'sum_over_time',
-  'count_over_time',
-  'quantile_over_time',
-  'stddev_over_time',
-  'stdvar_over_time',
+  {
+    insertText: 'abs()',
+    label: 'abs',
+    detail: 'abs(v instant-vector)',
+    documentation: 'Returns the input vector with all sample values converted to their absolute value.',
+  },
+  {
+    insertText: 'absent()',
+    label: 'absent',
+    detail: 'absent(v instant-vector)',
+    documentation:
+      'Returns an empty vector if the vector passed to it has any elements and a 1-element vector with the value 1 if the vector passed to it has no elements. This is useful for alerting on when no time series exist for a given metric name and label combination.',
+  },
+  {
+    insertText: 'ceil()',
+    label: 'ceil',
+    detail: 'ceil(v instant-vector)',
+    documentation: 'Rounds the sample values of all elements in `v` up to the nearest integer.',
+  },
+  {
+    insertText: 'changes()',
+    label: 'changes',
+    detail: 'changes(v range-vector)',
+    documentation:
+      'For each input time series, `changes(v range-vector)` returns the number of times its value has changed within the provided time range as an instant vector.',
+  },
+  {
+    insertText: 'clamp_max()',
+    label: 'clamp_max',
+    detail: 'clamp_max(v instant-vector, max scalar)',
+    documentation: 'Clamps the sample values of all elements in `v` to have an upper limit of `max`.',
+  },
+  {
+    insertText: 'clamp_min()',
+    label: 'clamp_min',
+    detail: 'clamp_min(v instant-vector, min scalar)',
+    documentation: 'Clamps the sample values of all elements in `v` to have a lower limit of `min`.',
+  },
+  {
+    insertText: 'count_scalar()',
+    label: 'count_scalar',
+    detail: 'count_scalar(v instant-vector)',
+    documentation:
+      'Returns the number of elements in a time series vector as a scalar. This is in contrast to the `count()` aggregation operator, which always returns a vector (an empty one if the input vector is empty) and allows grouping by labels via a `by` clause.',
+  },
+  {
+    insertText: 'day_of_month()',
+    label: 'day_of_month',
+    detail: 'day_of_month(v=vector(time()) instant-vector)',
+    documentation: 'Returns the day of the month for each of the given times in UTC. Returned values are from 1 to 31.',
+  },
+  {
+    insertText: 'day_of_week()',
+    label: 'day_of_week',
+    detail: 'day_of_week(v=vector(time()) instant-vector)',
+    documentation:
+      'Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.',
+  },
+  {
+    insertText: 'days_in_month()',
+    label: 'days_in_month',
+    detail: 'days_in_month(v=vector(time()) instant-vector)',
+    documentation:
+      'Returns number of days in the month for each of the given times in UTC. Returned values are from 28 to 31.',
+  },
+  {
+    insertText: 'delta()',
+    label: 'delta',
+    detail: 'delta(v range-vector)',
+    documentation:
+      'Calculates the difference between the first and last value of each time series element in a range vector `v`, returning an instant vector with the given deltas and equivalent labels. The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if the sample values are all integers.',
+  },
+  {
+    insertText: 'deriv()',
+    label: 'deriv',
+    detail: 'deriv(v range-vector)',
+    documentation:
+      'Calculates the per-second derivative of the time series in a range vector `v`, using simple linear regression.',
+  },
+  {
+    insertText: 'drop_common_labels()',
+    label: 'drop_common_labels',
+    detail: 'drop_common_labels(instant-vector)',
+    documentation: 'Drops all labels that have the same name and value across all series in the input vector.',
+  },
+  {
+    insertText: 'exp()',
+    label: 'exp',
+    detail: 'exp(v instant-vector)',
+    documentation:
+      'Calculates the exponential function for all elements in `v`.\nSpecial cases are:\n* `Exp(+Inf) = +Inf` \n* `Exp(NaN) = NaN`',
+  },
+  {
+    insertText: 'floor()',
+    label: 'floor',
+    detail: 'floor(v instant-vector)',
+    documentation: 'Rounds the sample values of all elements in `v` down to the nearest integer.',
+  },
+  {
+    insertText: 'histogram_quantile()',
+    label: 'histogram_quantile',
+    detail: 'histogram_quantile(φ float, b instant-vector)',
+    documentation:
+      'Calculates the φ-quantile (0 ≤ φ ≤ 1) from the buckets `b` of a histogram. The samples in `b` are the counts of observations in each bucket. Each sample must have a label `le` where the label value denotes the inclusive upper bound of the bucket. (Samples without such a label are silently ignored.) The histogram metric type automatically provides time series with the `_bucket` suffix and the appropriate labels.',
+  },
+  {
+    insertText: 'holt_winters()',
+    label: 'holt_winters',
+    detail: 'holt_winters(v range-vector, sf scalar, tf scalar)',
+    documentation:
+      'Produces a smoothed value for time series based on the range in `v`. The lower the smoothing factor `sf`, the more importance is given to old data. The higher the trend factor `tf`, the more trends in the data is considered. Both `sf` and `tf` must be between 0 and 1.',
+  },
+  {
+    insertText: 'hour()',
+    label: 'hour',
+    detail: 'hour(v=vector(time()) instant-vector)',
+    documentation: 'Returns the hour of the day for each of the given times in UTC. Returned values are from 0 to 23.',
+  },
+  {
+    insertText: 'idelta()',
+    label: 'idelta',
+    detail: 'idelta(v range-vector)',
+    documentation:
+      'Calculates the difference between the last two samples in the range vector `v`, returning an instant vector with the given deltas and equivalent labels.',
+  },
+  {
+    insertText: 'increase()',
+    label: 'increase',
+    detail: 'increase(v range-vector)',
+    documentation:
+      'Calculates the increase in the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if a counter increases only by integer increments.',
+  },
+  {
+    insertText: 'irate()',
+    label: 'irate',
+    detail: 'irate(v range-vector)',
+    documentation:
+      'Calculates the per-second instant rate of increase of the time series in the range vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for.',
+  },
+  {
+    insertText: 'label_replace()',
+    label: 'label_replace',
+    detail: 'label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)',
+    documentation:
+      "For each timeseries in `v`, `label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)`  matches the regular expression `regex` against the label `src_label`.  If it matches, then the timeseries is returned with the label `dst_label` replaced by the expansion of `replacement`. `$1` is replaced with the first matching subgroup, `$2` with the second etc. If the regular expression doesn't match then the timeseries is returned unchanged.",
+  },
+  {
+    insertText: 'ln()',
+    label: 'ln',
+    detail: 'ln(v instant-vector)',
+    documentation:
+      'calculates the natural logarithm for all elements in `v`.\nSpecial cases are:\n * `ln(+Inf) = +Inf`\n * `ln(0) = -Inf`\n * `ln(x < 0) = NaN`\n * `ln(NaN) = NaN`',
+  },
+  {
+    insertText: 'log2()',
+    label: 'log2',
+    detail: 'log2(v instant-vector)',
+    documentation:
+      'Calculates the binary logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.',
+  },
+  {
+    insertText: 'log10()',
+    label: 'log10',
+    detail: 'log10(v instant-vector)',
+    documentation:
+      'Calculates the decimal logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.',
+  },
+  {
+    insertText: 'minute()',
+    label: 'minute',
+    detail: 'minute(v=vector(time()) instant-vector)',
+    documentation:
+      'Returns the minute of the hour for each of the given times in UTC. Returned values are from 0 to 59.',
+  },
+  {
+    insertText: 'month()',
+    label: 'month',
+    detail: 'month(v=vector(time()) instant-vector)',
+    documentation:
+      'Returns the month of the year for each of the given times in UTC. Returned values are from 1 to 12, where 1 means January etc.',
+  },
+  {
+    insertText: 'predict_linear()',
+    label: 'predict_linear',
+    detail: 'predict_linear(v range-vector, t scalar)',
+    documentation:
+      'Predicts the value of time series `t` seconds from now, based on the range vector `v`, using simple linear regression.',
+  },
+  {
+    insertText: 'rate()',
+    label: 'rate',
+    detail: 'rate(v range-vector)',
+    documentation:
+      "Calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period.",
+  },
+  {
+    insertText: 'resets()',
+    label: 'resets',
+    detail: 'resets(v range-vector)',
+    documentation:
+      'For each input time series, `resets(v range-vector)` returns the number of counter resets within the provided time range as an instant vector. Any decrease in the value between two consecutive samples is interpreted as a counter reset.',
+  },
+  {
+    insertText: 'round()',
+    label: 'round',
+    detail: 'round(v instant-vector, to_nearest=1 scalar)',
+    documentation:
+      'Rounds the sample values of all elements in `v` to the nearest integer. Ties are resolved by rounding up. The optional `to_nearest` argument allows specifying the nearest multiple to which the sample values should be rounded. This multiple may also be a fraction.',
+  },
+  {
+    insertText: 'scalar()',
+    label: 'scalar',
+    detail: 'scalar(v instant-vector)',
+    documentation:
+      'Given a single-element input vector, `scalar(v instant-vector)` returns the sample value of that single element as a scalar. If the input vector does not have exactly one element, `scalar` will return `NaN`.',
+  },
+  {
+    insertText: 'sort()',
+    label: 'sort',
+    detail: 'sort(v instant-vector)',
+    documentation: 'Returns vector elements sorted by their sample values, in ascending order.',
+  },
+  {
+    insertText: 'sort_desc()',
+    label: 'sort_desc',
+    detail: 'sort_desc(v instant-vector)',
+    documentation: 'Returns vector elements sorted by their sample values, in descending order.',
+  },
+  {
+    insertText: 'sqrt()',
+    label: 'sqrt',
+    detail: 'sqrt(v instant-vector)',
+    documentation: 'Calculates the square root of all elements in `v`.',
+  },
+  {
+    insertText: 'time()',
+    label: 'time',
+    detail: 'time()',
+    documentation:
+      'Returns the number of seconds since January 1, 1970 UTC. Note that this does not actually return the current time, but the time at which the expression is to be evaluated.',
+  },
+  {
+    insertText: 'vector()',
+    label: 'vector',
+    detail: 'vector(s scalar)',
+    documentation: 'Returns the scalar `s` as a vector with no labels.',
+  },
+  {
+    insertText: 'year()',
+    label: 'year',
+    detail: 'year(v=vector(time()) instant-vector)',
+    documentation: 'Returns the year for each of the given times in UTC.',
+  },
+  {
+    insertText: 'avg_over_time()',
+    label: 'avg_over_time',
+    detail: 'avg_over_time(range-vector)',
+    documentation: 'The average value of all points in the specified interval.',
+  },
+  {
+    insertText: 'min_over_time()',
+    label: 'min_over_time',
+    detail: 'min_over_time(range-vector)',
+    documentation: 'The minimum value of all points in the specified interval.',
+  },
+  {
+    insertText: 'max_over_time()',
+    label: 'max_over_time',
+    detail: 'max_over_time(range-vector)',
+    documentation: 'The maximum value of all points in the specified interval.',
+  },
+  {
+    insertText: 'sum_over_time()',
+    label: 'sum_over_time',
+    detail: 'sum_over_time(range-vector)',
+    documentation: 'The sum of all values in the specified interval.',
+  },
+  {
+    insertText: 'count_over_time()',
+    label: 'count_over_time',
+    detail: 'count_over_time(range-vector)',
+    documentation: 'The count of all values in the specified interval.',
+  },
+  {
+    insertText: 'quantile_over_time()',
+    label: 'quantile_over_time',
+    detail: 'quantile_over_time(scalar, range-vector)',
+    documentation: 'The φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval.',
+  },
+  {
+    insertText: 'stddev_over_time()',
+    label: 'stddev_over_time',
+    detail: 'stddev_over_time(range-vector)',
+    documentation: 'The population standard deviation of the values in the specified interval.',
+  },
+  {
+    insertText: 'stdvar_over_time()',
+    label: 'stdvar_over_time',
+    detail: 'stdvar_over_time(range-vector)',
+    documentation: 'The population standard variance of the values in the specified interval.',
+  },
 ];
 
 const tokenizer = {
@@ -93,7 +394,7 @@ const tokenizer = {
       },
     },
   },
-  function: new RegExp(`\\b(?:${FUNCTIONS.join('|')})(?=\\s*\\()`, 'i'),
+  function: new RegExp(`\\b(?:${FUNCTIONS.map(f => f.label).join('|')})(?=\\s*\\()`, 'i'),
   'context-range': [
     {
       pattern: /\[[^\]]*(?=])/, // [1m]
diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss
index 119c468292a..10b2238f4b8 100644
--- a/public/sass/components/_slate_editor.scss
+++ b/public/sass/components/_slate_editor.scss
@@ -71,6 +71,7 @@
     .typeahead-item-hint {
       font-size: $font-size-xs;
       color: $text-color;
+      white-space: normal;
     }
   }
 }

From fc06f8bfe71d758148708dee23c52af678935a52 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 26 Jul 2018 17:22:15 +0200
Subject: [PATCH 104/380] Pass more tests

---
 public/app/plugins/panel/singlestat/module.ts |  1 +
 .../panel/singlestat/specs/singlestat.jest.ts | 34 ++++++++-----------
 2 files changed, 15 insertions(+), 20 deletions(-)

diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts
index ebd2628b086..7fafb5902d1 100644
--- a/public/app/plugins/panel/singlestat/module.ts
+++ b/public/app/plugins/panel/singlestat/module.ts
@@ -310,6 +310,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
         data.valueRounded = data.value;
         data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc());
       } else {
+        console.log(lastPoint, lastValue);
         data.value = this.series[0].stats[this.panel.valueName];
         data.flotpairs = this.series[0].flotpairs;
 
diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
index 2c945aa6eb2..7b89f86250c 100644
--- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
+++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
@@ -7,7 +7,7 @@ import moment from 'moment';
 describe('SingleStatCtrl', function() {
   let ctx = <any>{};
   let epoch = 1505826363746;
-  let clock;
+  Date.now = () => epoch;
 
   let $scope = {
     $on: () => {},
@@ -24,7 +24,7 @@ describe('SingleStatCtrl', function() {
     },
   };
   SingleStatCtrl.prototype.dashboard = {
-    isTimezoneUtc: () => {},
+    isTimezoneUtc: jest.fn(() => true),
   };
 
   function singleStatScenario(desc, func) {
@@ -89,29 +89,30 @@ describe('SingleStatCtrl', function() {
       ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsIso';
+      ctx.ctrl.dashboard.isTimezoneUtc = () => false;
     });
 
     it('Should use time instead of value', function() {
-      console.log(ctx.data.value);
       expect(ctx.data.value).toBe(1505634997920);
       expect(ctx.data.valueRounded).toBe(1505634997920);
     });
 
     it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).toBe(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss'));
+      expect(ctx.data.valueFormatted).toBe('2017-09-17 09:56:37');
     });
   });
 
   singleStatScenario('showing last iso time instead of value (in UTC)', function(ctx) {
     ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsIso';
       //   ctx.setIsUtc(true);
+      ctx.ctrl.dashboard.isTimezoneUtc = () => true;
     });
 
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).toBe(moment.utc(1505634997920).format('YYYY-MM-DD HH:mm:ss'));
+    it('should set value', function() {
+      expect(ctx.data.valueFormatted).toBe('1970-01-01 00:00:05');
     });
   });
 
@@ -120,6 +121,7 @@ describe('SingleStatCtrl', function() {
       ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsUS';
+      ctx.ctrl.dashboard.isTimezoneUtc = () => false;
     });
 
     it('Should use time instead of value', function() {
@@ -134,21 +136,22 @@ describe('SingleStatCtrl', function() {
 
   singleStatScenario('showing last us time instead of value (in UTC)', function(ctx) {
     ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
+      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsUS';
       //   ctx.setIsUtc(true);
+      ctx.ctrl.dashboard.isTimezoneUtc = () => true;
     });
 
     it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).toBe(moment.utc(1505634997920).format('MM/DD/YYYY h:mm:ss a'));
+      expect(ctx.data.valueFormatted).toBe('01/01/1970 12:00:05 am');
     });
   });
 
   singleStatScenario('showing last time from now instead of value', function(ctx) {
     beforeEach(() => {
       //   clock = sinon.useFakeTimers(epoch);
-      jest.useFakeTimers();
+      //jest.useFakeTimers();
     });
 
     ctx.setup(function() {
@@ -167,16 +170,11 @@ describe('SingleStatCtrl', function() {
     });
 
     afterEach(() => {
-      jest.clearAllTimers();
+      //   jest.clearAllTimers();
     });
   });
 
   singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) {
-    beforeEach(() => {
-      //   clock = sinon.useFakeTimers(epoch);
-      jest.useFakeTimers();
-    });
-
     ctx.setup(function() {
       ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
@@ -187,10 +185,6 @@ describe('SingleStatCtrl', function() {
     it('should set formatted value', function() {
       expect(ctx.data.valueFormatted).toBe('2 days ago');
     });
-
-    afterEach(() => {
-      jest.clearAllTimers();
-    });
   });
 
   singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function(

From d42cea5d42c58175448986a8682b7a8c137be088 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 26 Jul 2018 18:09:42 +0200
Subject: [PATCH 105/380] refactor sql engine to make it hold all common code
 for sql datasources

---
 pkg/tsdb/sql_engine.go | 324 +++++++++++++++++++++++++++++++++++------
 1 file changed, 279 insertions(+), 45 deletions(-)

diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index ec908aeb9de..9321e8912dc 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -1,11 +1,17 @@
 package tsdb
 
 import (
+	"container/list"
 	"context"
+	"database/sql"
 	"fmt"
+	"math"
+	"strings"
 	"sync"
 	"time"
 
+	"github.com/grafana/grafana/pkg/log"
+
 	"github.com/grafana/grafana/pkg/components/null"
 
 	"github.com/go-xorm/core"
@@ -14,27 +20,15 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 )
 
-// SqlEngine is a wrapper class around xorm for relational database data sources.
-type SqlEngine interface {
-	InitEngine(driverName string, dsInfo *models.DataSource, cnnstr string) error
-	Query(
-		ctx context.Context,
-		ds *models.DataSource,
-		query *TsdbQuery,
-		transformToTimeSeries func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error,
-		transformToTable func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error,
-	) (*Response, error)
-}
-
 // SqlMacroEngine interpolates macros into sql. It takes in the Query to have access to query context and
 // timeRange to be able to generate queries that use from and to.
 type SqlMacroEngine interface {
 	Interpolate(query *Query, timeRange *TimeRange, sql string) (string, error)
 }
 
-type DefaultSqlEngine struct {
-	MacroEngine SqlMacroEngine
-	XormEngine  *xorm.Engine
+// SqlTableRowTransformer transforms a query result row to RowValues with proper types.
+type SqlTableRowTransformer interface {
+	Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (RowValues, error)
 }
 
 type engineCacheType struct {
@@ -48,69 +42,92 @@ var engineCache = engineCacheType{
 	versions: make(map[int64]int),
 }
 
-// InitEngine creates the db connection and inits the xorm engine or loads it from the engine cache
-func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSource, cnnstr string) error {
+var NewXormEngine = func(driverName string, connectionString string) (*xorm.Engine, error) {
+	return xorm.NewEngine(driverName, connectionString)
+}
+
+type sqlQueryEndpoint struct {
+	macroEngine       SqlMacroEngine
+	rowTransformer    SqlTableRowTransformer
+	engine            *xorm.Engine
+	timeColumnNames   []string
+	metricColumnTypes []string
+	log               log.Logger
+}
+
+type SqlQueryEndpointConfiguration struct {
+	DriverName        string
+	Datasource        *models.DataSource
+	ConnectionString  string
+	TimeColumnNames   []string
+	MetricColumnTypes []string
+}
+
+var NewSqlQueryEndpoint = func(config *SqlQueryEndpointConfiguration, rowTransformer SqlTableRowTransformer, macroEngine SqlMacroEngine, log log.Logger) (TsdbQueryEndpoint, error) {
+	queryEndpoint := sqlQueryEndpoint{
+		rowTransformer:  rowTransformer,
+		macroEngine:     macroEngine,
+		timeColumnNames: []string{"time"},
+		log:             log,
+	}
+
+	if len(config.TimeColumnNames) > 0 {
+		queryEndpoint.timeColumnNames = config.TimeColumnNames
+	}
+
 	engineCache.Lock()
 	defer engineCache.Unlock()
 
-	if engine, present := engineCache.cache[dsInfo.Id]; present {
-		if version := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
-			e.XormEngine = engine
-			return nil
+	if engine, present := engineCache.cache[config.Datasource.Id]; present {
+		if version := engineCache.versions[config.Datasource.Id]; version == config.Datasource.Version {
+			queryEndpoint.engine = engine
+			return &queryEndpoint, nil
 		}
 	}
 
-	engine, err := xorm.NewEngine(driverName, cnnstr)
+	engine, err := NewXormEngine(config.DriverName, config.ConnectionString)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	engine.SetMaxOpenConns(10)
 	engine.SetMaxIdleConns(10)
 
-	engineCache.versions[dsInfo.Id] = dsInfo.Version
-	engineCache.cache[dsInfo.Id] = engine
-	e.XormEngine = engine
+	engineCache.versions[config.Datasource.Id] = config.Datasource.Version
+	engineCache.cache[config.Datasource.Id] = engine
+	queryEndpoint.engine = engine
 
-	return nil
+	return &queryEndpoint, nil
 }
 
-// Query is a default implementation of the Query method for an SQL data source.
-// The caller of this function must implement transformToTimeSeries and transformToTable and
-// pass them in as parameters.
-func (e *DefaultSqlEngine) Query(
-	ctx context.Context,
-	dsInfo *models.DataSource,
-	tsdbQuery *TsdbQuery,
-	transformToTimeSeries func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error,
-	transformToTable func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error,
-) (*Response, error) {
+// Query is the main function for the SqlQueryEndpoint
+func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *TsdbQuery) (*Response, error) {
 	result := &Response{
 		Results: make(map[string]*QueryResult),
 	}
 
-	session := e.XormEngine.NewSession()
+	session := e.engine.NewSession()
 	defer session.Close()
 	db := session.DB()
 
 	for _, query := range tsdbQuery.Queries {
-		rawSql := query.Model.Get("rawSql").MustString()
-		if rawSql == "" {
+		rawSQL := query.Model.Get("rawSql").MustString()
+		if rawSQL == "" {
 			continue
 		}
 
 		queryResult := &QueryResult{Meta: simplejson.New(), RefId: query.RefId}
 		result.Results[query.RefId] = queryResult
 
-		rawSql, err := e.MacroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSql)
+		rawSQL, err := e.macroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSQL)
 		if err != nil {
 			queryResult.Error = err
 			continue
 		}
 
-		queryResult.Meta.Set("sql", rawSql)
+		queryResult.Meta.Set("sql", rawSQL)
 
-		rows, err := db.Query(rawSql)
+		rows, err := db.Query(rawSQL)
 		if err != nil {
 			queryResult.Error = err
 			continue
@@ -122,13 +139,13 @@ func (e *DefaultSqlEngine) Query(
 
 		switch format {
 		case "time_series":
-			err := transformToTimeSeries(query, rows, queryResult, tsdbQuery)
+			err := e.transformToTimeSeries(query, rows, queryResult, tsdbQuery)
 			if err != nil {
 				queryResult.Error = err
 				continue
 			}
 		case "table":
-			err := transformToTable(query, rows, queryResult, tsdbQuery)
+			err := e.transformToTable(query, rows, queryResult, tsdbQuery)
 			if err != nil {
 				queryResult.Error = err
 				continue
@@ -139,6 +156,223 @@ func (e *DefaultSqlEngine) Query(
 	return result, nil
 }
 
+func (e *sqlQueryEndpoint) transformToTable(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error {
+	columnNames, err := rows.Columns()
+	columnCount := len(columnNames)
+
+	if err != nil {
+		return err
+	}
+
+	rowLimit := 1000000
+	rowCount := 0
+	timeIndex := -1
+
+	table := &Table{
+		Columns: make([]TableColumn, columnCount),
+		Rows:    make([]RowValues, 0),
+	}
+
+	for i, name := range columnNames {
+		table.Columns[i].Text = name
+
+		for _, tc := range e.timeColumnNames {
+			if name == tc {
+				timeIndex = i
+				break
+			}
+		}
+	}
+
+	columnTypes, err := rows.ColumnTypes()
+	if err != nil {
+		return err
+	}
+
+	for ; rows.Next(); rowCount++ {
+		if rowCount > rowLimit {
+			return fmt.Errorf("query row limit exceeded, limit %d", rowLimit)
+		}
+
+		values, err := e.rowTransformer.Transform(columnTypes, rows)
+		if err != nil {
+			return err
+		}
+
+		// converts column named time to unix timestamp in milliseconds
+		// to make native mssql datetime types and epoch dates work in
+		// annotation and table queries.
+		ConvertSqlTimeColumnToEpochMs(values, timeIndex)
+		table.Rows = append(table.Rows, values)
+	}
+
+	result.Tables = append(result.Tables, table)
+	result.Meta.Set("rowCount", rowCount)
+	return nil
+}
+
+func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error {
+	pointsBySeries := make(map[string]*TimeSeries)
+	seriesByQueryOrder := list.New()
+
+	columnNames, err := rows.Columns()
+	if err != nil {
+		return err
+	}
+
+	columnTypes, err := rows.ColumnTypes()
+	if err != nil {
+		return err
+	}
+
+	rowLimit := 1000000
+	rowCount := 0
+	timeIndex := -1
+	metricIndex := -1
+
+	// check columns of resultset: a column named time is mandatory
+	// the first text column is treated as metric name unless a column named metric is present
+	for i, col := range columnNames {
+		for _, tc := range e.timeColumnNames {
+			if col == tc {
+				timeIndex = i
+				continue
+			}
+		}
+		switch col {
+		case "metric":
+			metricIndex = i
+		default:
+			if metricIndex == -1 {
+				columnType := columnTypes[i].DatabaseTypeName()
+
+				for _, mct := range e.metricColumnTypes {
+					if columnType == mct {
+						metricIndex = i
+						continue
+					}
+				}
+			}
+		}
+	}
+
+	if timeIndex == -1 {
+		return fmt.Errorf("Found no column named %s", strings.Join(e.timeColumnNames, " or "))
+	}
+
+	fillMissing := query.Model.Get("fill").MustBool(false)
+	var fillInterval float64
+	fillValue := null.Float{}
+	if fillMissing {
+		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
+		if !query.Model.Get("fillNull").MustBool(false) {
+			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
+			fillValue.Valid = true
+		}
+	}
+
+	for rows.Next() {
+		var timestamp float64
+		var value null.Float
+		var metric string
+
+		if rowCount > rowLimit {
+			return fmt.Errorf("query row limit exceeded, limit %d", rowLimit)
+		}
+
+		values, err := e.rowTransformer.Transform(columnTypes, rows)
+		if err != nil {
+			return err
+		}
+
+		// converts column named time to unix timestamp in milliseconds to make
+		// native mysql datetime types and epoch dates work in
+		// annotation and table queries.
+		ConvertSqlTimeColumnToEpochMs(values, timeIndex)
+
+		switch columnValue := values[timeIndex].(type) {
+		case int64:
+			timestamp = float64(columnValue)
+		case float64:
+			timestamp = columnValue
+		default:
+			return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
+		}
+
+		if metricIndex >= 0 {
+			if columnValue, ok := values[metricIndex].(string); ok {
+				metric = columnValue
+			} else {
+				return fmt.Errorf("Column metric must be of type %s. metric column name: %s type: %s but datatype is %T", strings.Join(e.metricColumnTypes, ", "), columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
+			}
+		}
+
+		for i, col := range columnNames {
+			if i == timeIndex || i == metricIndex {
+				continue
+			}
+
+			if value, err = ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
+				return err
+			}
+
+			if metricIndex == -1 {
+				metric = col
+			}
+
+			series, exist := pointsBySeries[metric]
+			if !exist {
+				series = &TimeSeries{Name: metric}
+				pointsBySeries[metric] = series
+				seriesByQueryOrder.PushBack(metric)
+			}
+
+			if fillMissing {
+				var intervalStart float64
+				if !exist {
+					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
+				} else {
+					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
+				}
+
+				// align interval start
+				intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
+
+				for i := intervalStart; i < timestamp; i += fillInterval {
+					series.Points = append(series.Points, TimePoint{fillValue, null.FloatFrom(i)})
+					rowCount++
+				}
+			}
+
+			series.Points = append(series.Points, TimePoint{value, null.FloatFrom(timestamp)})
+
+			e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
+		}
+	}
+
+	for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
+		key := elem.Value.(string)
+		result.Series = append(result.Series, pointsBySeries[key])
+
+		if fillMissing {
+			series := pointsBySeries[key]
+			// fill in values from last fetched value till interval end
+			intervalStart := series.Points[len(series.Points)-1][1].Float64
+			intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
+
+			// align interval start
+			intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
+			for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
+				series.Points = append(series.Points, TimePoint{fillValue, null.FloatFrom(i)})
+				rowCount++
+			}
+		}
+	}
+
+	result.Meta.Set("rowCount", rowCount)
+	return nil
+}
+
 // ConvertSqlTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds
 // to make native datetime types and epoch dates work in annotation and table queries.
 func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) {

From 2f3851b915620040204919b17b603c5b07a7de1a Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 26 Jul 2018 18:10:17 +0200
Subject: [PATCH 106/380] postgres: use new sql engine

---
 pkg/tsdb/postgres/macros.go        |  38 ++--
 pkg/tsdb/postgres/macros_test.go   |   2 +-
 pkg/tsdb/postgres/postgres.go      | 269 +++--------------------------
 pkg/tsdb/postgres/postgres_test.go |  30 ++--
 4 files changed, 64 insertions(+), 275 deletions(-)

diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 61e88418ff4..661dbf3d4ce 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -14,18 +14,18 @@ import (
 const rsIdentifier = `([_a-zA-Z0-9]+)`
 const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)`
 
-type PostgresMacroEngine struct {
-	TimeRange *tsdb.TimeRange
-	Query     *tsdb.Query
+type postgresMacroEngine struct {
+	timeRange *tsdb.TimeRange
+	query     *tsdb.Query
 }
 
-func NewPostgresMacroEngine() tsdb.SqlMacroEngine {
-	return &PostgresMacroEngine{}
+func newPostgresMacroEngine() tsdb.SqlMacroEngine {
+	return &postgresMacroEngine{}
 }
 
-func (m *PostgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
-	m.TimeRange = timeRange
-	m.Query = query
+func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+	m.timeRange = timeRange
+	m.query = query
 	rExp, _ := regexp.Compile(sExpr)
 	var macroError error
 
@@ -66,7 +66,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str
 	return result + str[lastIndex:]
 }
 
-func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, error) {
+func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, error) {
 	switch name {
 	case "__time":
 		if len(args) == 0 {
@@ -83,11 +83,11 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 
-		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeFrom":
-		return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeTo":
-		return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeGroup":
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
@@ -97,16 +97,16 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			return "", fmt.Errorf("error parsing interval %v", args[1])
 		}
 		if len(args) == 3 {
-			m.Query.Model.Set("fill", true)
-			m.Query.Model.Set("fillInterval", interval.Seconds())
+			m.query.Model.Set("fill", true)
+			m.query.Model.Set("fillInterval", interval.Seconds())
 			if args[2] == "NULL" {
-				m.Query.Model.Set("fillNull", true)
+				m.query.Model.Set("fillNull", true)
 			} else {
 				floatVal, err := strconv.ParseFloat(args[2], 64)
 				if err != nil {
 					return "", fmt.Errorf("error parsing fill value %v", args[2])
 				}
-				m.Query.Model.Set("fillValue", floatVal)
+				m.query.Model.Set("fillValue", floatVal)
 			}
 		}
 		return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
@@ -114,11 +114,11 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
-		return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
+		return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil
 	case "__unixEpochFrom":
-		return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
+		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
-		return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
+		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index 8c581850430..194573be0fd 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -12,7 +12,7 @@ import (
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		engine := NewPostgresMacroEngine()
+		engine := newPostgresMacroEngine()
 		query := &tsdb.Query{}
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index f19e4fb54f4..b9f333db127 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -1,46 +1,38 @@
 package postgres
 
 import (
-	"container/list"
-	"context"
-	"fmt"
-	"math"
+	"database/sql"
 	"net/url"
 	"strconv"
 
 	"github.com/go-xorm/core"
-	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
-type PostgresQueryEndpoint struct {
-	sqlEngine tsdb.SqlEngine
-	log       log.Logger
-}
-
 func init() {
-	tsdb.RegisterTsdbQueryEndpoint("postgres", NewPostgresQueryEndpoint)
+	tsdb.RegisterTsdbQueryEndpoint("postgres", newPostgresQueryEndpoint)
 }
 
-func NewPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
-	endpoint := &PostgresQueryEndpoint{
-		log: log.New("tsdb.postgres"),
-	}
-
-	endpoint.sqlEngine = &tsdb.DefaultSqlEngine{
-		MacroEngine: NewPostgresMacroEngine(),
-	}
+func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
+	logger := log.New("tsdb.postgres")
 
 	cnnstr := generateConnectionString(datasource)
-	endpoint.log.Debug("getEngine", "connection", cnnstr)
+	logger.Debug("getEngine", "connection", cnnstr)
 
-	if err := endpoint.sqlEngine.InitEngine("postgres", datasource, cnnstr); err != nil {
-		return nil, err
+	config := tsdb.SqlQueryEndpointConfiguration{
+		DriverName:        "postgres",
+		ConnectionString:  cnnstr,
+		Datasource:        datasource,
+		MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"},
 	}
 
-	return endpoint, nil
+	rowTransformer := postgresRowTransformer{
+		log: logger,
+	}
+
+	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(), logger)
 }
 
 func generateConnectionString(datasource *models.DataSource) string {
@@ -63,70 +55,15 @@ func generateConnectionString(datasource *models.DataSource) string {
 	return u.String()
 }
 
-func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
-	return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable)
+type postgresRowTransformer struct {
+	log log.Logger
 }
 
-func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
-	columnNames, err := rows.Columns()
-	if err != nil {
-		return err
-	}
+func (t *postgresRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
+	values := make([]interface{}, len(columnTypes))
+	valuePtrs := make([]interface{}, len(columnTypes))
 
-	table := &tsdb.Table{
-		Columns: make([]tsdb.TableColumn, len(columnNames)),
-		Rows:    make([]tsdb.RowValues, 0),
-	}
-
-	for i, name := range columnNames {
-		table.Columns[i].Text = name
-	}
-
-	rowLimit := 1000000
-	rowCount := 0
-	timeIndex := -1
-
-	// check if there is a column named time
-	for i, col := range columnNames {
-		switch col {
-		case "time":
-			timeIndex = i
-		}
-	}
-
-	for ; rows.Next(); rowCount++ {
-		if rowCount > rowLimit {
-			return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit)
-		}
-
-		values, err := e.getTypedRowData(rows)
-		if err != nil {
-			return err
-		}
-
-		// converts column named time to unix timestamp in milliseconds to make
-		// native postgres datetime types and epoch dates work in
-		// annotation and table queries.
-		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
-		table.Rows = append(table.Rows, values)
-	}
-
-	result.Tables = append(result.Tables, table)
-	result.Meta.Set("rowCount", rowCount)
-	return nil
-}
-
-func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) {
-	types, err := rows.ColumnTypes()
-	if err != nil {
-		return nil, err
-	}
-
-	values := make([]interface{}, len(types))
-	valuePtrs := make([]interface{}, len(types))
-
-	for i := 0; i < len(types); i++ {
+	for i := 0; i < len(columnTypes); i++ {
 		valuePtrs[i] = &values[i]
 	}
 
@@ -136,20 +73,20 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
 
 	// convert types not handled by lib/pq
 	// unhandled types are returned as []byte
-	for i := 0; i < len(types); i++ {
+	for i := 0; i < len(columnTypes); i++ {
 		if value, ok := values[i].([]byte); ok {
-			switch types[i].DatabaseTypeName() {
+			switch columnTypes[i].DatabaseTypeName() {
 			case "NUMERIC":
 				if v, err := strconv.ParseFloat(string(value), 64); err == nil {
 					values[i] = v
 				} else {
-					e.log.Debug("Rows", "Error converting numeric to float", value)
+					t.log.Debug("Rows", "Error converting numeric to float", value)
 				}
 			case "UNKNOWN", "CIDR", "INET", "MACADDR":
 				// char literals have type UNKNOWN
 				values[i] = string(value)
 			default:
-				e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value)
+				t.log.Debug("Rows", "Unknown database type", columnTypes[i].DatabaseTypeName(), "value", value)
 				values[i] = string(value)
 			}
 		}
@@ -157,159 +94,3 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
 
 	return values, nil
 }
-
-func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
-	pointsBySeries := make(map[string]*tsdb.TimeSeries)
-	seriesByQueryOrder := list.New()
-
-	columnNames, err := rows.Columns()
-	if err != nil {
-		return err
-	}
-
-	columnTypes, err := rows.ColumnTypes()
-	if err != nil {
-		return err
-	}
-
-	rowLimit := 1000000
-	rowCount := 0
-	timeIndex := -1
-	metricIndex := -1
-
-	// check columns of resultset: a column named time is mandatory
-	// the first text column is treated as metric name unless a column named metric is present
-	for i, col := range columnNames {
-		switch col {
-		case "time":
-			timeIndex = i
-		case "metric":
-			metricIndex = i
-		default:
-			if metricIndex == -1 {
-				switch columnTypes[i].DatabaseTypeName() {
-				case "UNKNOWN", "TEXT", "VARCHAR", "CHAR":
-					metricIndex = i
-				}
-			}
-		}
-	}
-
-	if timeIndex == -1 {
-		return fmt.Errorf("Found no column named time")
-	}
-
-	fillMissing := query.Model.Get("fill").MustBool(false)
-	var fillInterval float64
-	fillValue := null.Float{}
-	if fillMissing {
-		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if !query.Model.Get("fillNull").MustBool(false) {
-			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
-			fillValue.Valid = true
-		}
-	}
-
-	for rows.Next() {
-		var timestamp float64
-		var value null.Float
-		var metric string
-
-		if rowCount > rowLimit {
-			return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit)
-		}
-
-		values, err := e.getTypedRowData(rows)
-		if err != nil {
-			return err
-		}
-
-		// converts column named time to unix timestamp in milliseconds to make
-		// native mysql datetime types and epoch dates work in
-		// annotation and table queries.
-		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
-		switch columnValue := values[timeIndex].(type) {
-		case int64:
-			timestamp = float64(columnValue)
-		case float64:
-			timestamp = columnValue
-		default:
-			return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
-		}
-
-		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok {
-				metric = columnValue
-			} else {
-				return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
-			}
-		}
-
-		for i, col := range columnNames {
-			if i == timeIndex || i == metricIndex {
-				continue
-			}
-
-			if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
-				return err
-			}
-
-			if metricIndex == -1 {
-				metric = col
-			}
-
-			series, exist := pointsBySeries[metric]
-			if !exist {
-				series = &tsdb.TimeSeries{Name: metric}
-				pointsBySeries[metric] = series
-				seriesByQueryOrder.PushBack(metric)
-			}
-
-			if fillMissing {
-				var intervalStart float64
-				if !exist {
-					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
-				} else {
-					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
-				}
-
-				// align interval start
-				intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-
-				for i := intervalStart; i < timestamp; i += fillInterval {
-					series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
-					rowCount++
-				}
-			}
-
-			series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)})
-
-			e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
-			rowCount++
-
-		}
-	}
-
-	for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
-		key := elem.Value.(string)
-		result.Series = append(result.Series, pointsBySeries[key])
-
-		if fillMissing {
-			series := pointsBySeries[key]
-			// fill in values from last fetched value till interval end
-			intervalStart := series.Points[len(series.Points)-1][1].Float64
-			intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
-
-			// align interval start
-			intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-			for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
-				series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
-				rowCount++
-			}
-		}
-	}
-
-	result.Meta.Set("rowCount", rowCount)
-	return nil
-}
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index a3a6d6546df..089829bf590 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -8,8 +8,9 @@ import (
 	"time"
 
 	"github.com/go-xorm/xorm"
+	"github.com/grafana/grafana/pkg/components/securejsondata"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/sqlstore"
 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
 	"github.com/grafana/grafana/pkg/tsdb"
@@ -22,8 +23,9 @@ import (
 // The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest!
 // Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a
 // preconfigured Postgres server suitable for running these tests.
-// There is also a dashboard.json in same directory that you can import to Grafana
-// once you've created a datasource for the test server/database.
+// There is also a datasource and dashboard provisioned by devenv scripts that you can
+// use to verify that the generated data are vizualized as expected, see
+// devenv/README.md for setup instructions.
 func TestPostgres(t *testing.T) {
 	// change to true to run the MySQL tests
 	runPostgresTests := false
@@ -36,19 +38,25 @@ func TestPostgres(t *testing.T) {
 	Convey("PostgreSQL", t, func() {
 		x := InitPostgresTestDB(t)
 
-		endpoint := &PostgresQueryEndpoint{
-			sqlEngine: &tsdb.DefaultSqlEngine{
-				MacroEngine: NewPostgresMacroEngine(),
-				XormEngine:  x,
-			},
-			log: log.New("tsdb.postgres"),
+		origXormEngine := tsdb.NewXormEngine
+		tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) {
+			return x, nil
 		}
 
-		sess := x.NewSession()
-		defer sess.Close()
+		endpoint, err := newPostgresQueryEndpoint(&models.DataSource{
+			JsonData:       simplejson.New(),
+			SecureJsonData: securejsondata.SecureJsonData{},
+		})
+		So(err, ShouldBeNil)
 
+		sess := x.NewSession()
 		fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
 
+		Reset(func() {
+			sess.Close()
+			tsdb.NewXormEngine = origXormEngine
+		})
+
 		Convey("Given a table with different native data types", func() {
 			sql := `
 				DROP TABLE IF EXISTS postgres_types;

From 27db4540125ae1c5d342319fade4043bc2221081 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 26 Jul 2018 18:10:45 +0200
Subject: [PATCH 107/380] mysql: use new sql engine

---
 pkg/tsdb/mysql/macros.go      |  38 ++---
 pkg/tsdb/mysql/macros_test.go |   2 +-
 pkg/tsdb/mysql/mysql.go       | 267 +++-------------------------------
 pkg/tsdb/mysql/mysql_test.go  |  30 ++--
 4 files changed, 62 insertions(+), 275 deletions(-)

diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index 584f731f3b8..078d1ff54f8 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -14,18 +14,18 @@ import (
 const rsIdentifier = `([_a-zA-Z0-9]+)`
 const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)`
 
-type MySqlMacroEngine struct {
-	TimeRange *tsdb.TimeRange
-	Query     *tsdb.Query
+type mySqlMacroEngine struct {
+	timeRange *tsdb.TimeRange
+	query     *tsdb.Query
 }
 
-func NewMysqlMacroEngine() tsdb.SqlMacroEngine {
-	return &MySqlMacroEngine{}
+func newMysqlMacroEngine() tsdb.SqlMacroEngine {
+	return &mySqlMacroEngine{}
 }
 
-func (m *MySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
-	m.TimeRange = timeRange
-	m.Query = query
+func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+	m.timeRange = timeRange
+	m.query = query
 	rExp, _ := regexp.Compile(sExpr)
 	var macroError error
 
@@ -66,7 +66,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str
 	return result + str[lastIndex:]
 }
 
-func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
+func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
 	switch name {
 	case "__timeEpoch", "__time":
 		if len(args) == 0 {
@@ -78,11 +78,11 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 
-		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeFrom":
-		return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeTo":
-		return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeGroup":
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval", name)
@@ -92,16 +92,16 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			return "", fmt.Errorf("error parsing interval %v", args[1])
 		}
 		if len(args) == 3 {
-			m.Query.Model.Set("fill", true)
-			m.Query.Model.Set("fillInterval", interval.Seconds())
+			m.query.Model.Set("fill", true)
+			m.query.Model.Set("fillInterval", interval.Seconds())
 			if args[2] == "NULL" {
-				m.Query.Model.Set("fillNull", true)
+				m.query.Model.Set("fillNull", true)
 			} else {
 				floatVal, err := strconv.ParseFloat(args[2], 64)
 				if err != nil {
 					return "", fmt.Errorf("error parsing fill value %v", args[2])
 				}
-				m.Query.Model.Set("fillValue", floatVal)
+				m.query.Model.Set("fillValue", floatVal)
 			}
 		}
 		return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil
@@ -109,11 +109,11 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
-		return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
+		return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil
 	case "__unixEpochFrom":
-		return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
+		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
-		return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
+		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}
diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go
index 2561661b385..003af9a737f 100644
--- a/pkg/tsdb/mysql/macros_test.go
+++ b/pkg/tsdb/mysql/macros_test.go
@@ -12,7 +12,7 @@ import (
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		engine := &MySqlMacroEngine{}
+		engine := &mySqlMacroEngine{}
 		query := &tsdb.Query{}
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go
index 7eceaffdb09..645f6b49bbb 100644
--- a/pkg/tsdb/mysql/mysql.go
+++ b/pkg/tsdb/mysql/mysql.go
@@ -1,39 +1,24 @@
 package mysql
 
 import (
-	"container/list"
-	"context"
 	"database/sql"
 	"fmt"
-	"math"
 	"reflect"
 	"strconv"
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/go-xorm/core"
-	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
-type MysqlQueryEndpoint struct {
-	sqlEngine tsdb.SqlEngine
-	log       log.Logger
-}
-
 func init() {
-	tsdb.RegisterTsdbQueryEndpoint("mysql", NewMysqlQueryEndpoint)
+	tsdb.RegisterTsdbQueryEndpoint("mysql", newMysqlQueryEndpoint)
 }
 
-func NewMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
-	endpoint := &MysqlQueryEndpoint{
-		log: log.New("tsdb.mysql"),
-	}
-
-	endpoint.sqlEngine = &tsdb.DefaultSqlEngine{
-		MacroEngine: NewMysqlMacroEngine(),
-	}
+func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
+	logger := log.New("tsdb.mysql")
 
 	cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true",
 		datasource.User,
@@ -42,85 +27,35 @@ func NewMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
 		datasource.Url,
 		datasource.Database,
 	)
-	endpoint.log.Debug("getEngine", "connection", cnnstr)
+	logger.Debug("getEngine", "connection", cnnstr)
 
-	if err := endpoint.sqlEngine.InitEngine("mysql", datasource, cnnstr); err != nil {
-		return nil, err
+	config := tsdb.SqlQueryEndpointConfiguration{
+		DriverName:        "mysql",
+		ConnectionString:  cnnstr,
+		Datasource:        datasource,
+		TimeColumnNames:   []string{"time", "time_sec"},
+		MetricColumnTypes: []string{"CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"},
 	}
 
-	return endpoint, nil
+	rowTransformer := mysqlRowTransformer{
+		log: logger,
+	}
+
+	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMysqlMacroEngine(), logger)
 }
 
-// Query is the main function for the MysqlExecutor
-func (e *MysqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
-	return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable)
+type mysqlRowTransformer struct {
+	log log.Logger
 }
 
-func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
-	columnNames, err := rows.Columns()
-	columnCount := len(columnNames)
-
-	if err != nil {
-		return err
-	}
-
-	table := &tsdb.Table{
-		Columns: make([]tsdb.TableColumn, columnCount),
-		Rows:    make([]tsdb.RowValues, 0),
-	}
-
-	for i, name := range columnNames {
-		table.Columns[i].Text = name
-	}
-
-	rowLimit := 1000000
-	rowCount := 0
-	timeIndex := -1
-
-	// check if there is a column named time
-	for i, col := range columnNames {
-		switch col {
-		case "time", "time_sec":
-			timeIndex = i
-		}
-	}
-
-	for ; rows.Next(); rowCount++ {
-		if rowCount > rowLimit {
-			return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
-		}
-
-		values, err := e.getTypedRowData(rows)
-		if err != nil {
-			return err
-		}
-
-		// converts column named time to unix timestamp in milliseconds to make
-		// native mysql datetime types and epoch dates work in
-		// annotation and table queries.
-		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
-		table.Rows = append(table.Rows, values)
-	}
-
-	result.Tables = append(result.Tables, table)
-	result.Meta.Set("rowCount", rowCount)
-	return nil
-}
-
-func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) {
-	types, err := rows.ColumnTypes()
-	if err != nil {
-		return nil, err
-	}
-
-	values := make([]interface{}, len(types))
+func (t *mysqlRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
+	values := make([]interface{}, len(columnTypes))
 
 	for i := range values {
-		scanType := types[i].ScanType()
+		scanType := columnTypes[i].ScanType()
 		values[i] = reflect.New(scanType).Interface()
 
-		if types[i].DatabaseTypeName() == "BIT" {
+		if columnTypes[i].DatabaseTypeName() == "BIT" {
 			values[i] = new([]byte)
 		}
 	}
@@ -129,7 +64,7 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er
 		return nil, err
 	}
 
-	for i := 0; i < len(types); i++ {
+	for i := 0; i < len(columnTypes); i++ {
 		typeName := reflect.ValueOf(values[i]).Type().String()
 
 		switch typeName {
@@ -158,7 +93,7 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er
 			}
 		}
 
-		if types[i].DatabaseTypeName() == "DECIMAL" {
+		if columnTypes[i].DatabaseTypeName() == "DECIMAL" {
 			f, err := strconv.ParseFloat(values[i].(string), 64)
 
 			if err == nil {
@@ -171,159 +106,3 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er
 
 	return values, nil
 }
-
-func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
-	pointsBySeries := make(map[string]*tsdb.TimeSeries)
-	seriesByQueryOrder := list.New()
-
-	columnNames, err := rows.Columns()
-	if err != nil {
-		return err
-	}
-
-	columnTypes, err := rows.ColumnTypes()
-	if err != nil {
-		return err
-	}
-
-	rowLimit := 1000000
-	rowCount := 0
-	timeIndex := -1
-	metricIndex := -1
-
-	// check columns of resultset: a column named time is mandatory
-	// the first text column is treated as metric name unless a column named metric is present
-	for i, col := range columnNames {
-		switch col {
-		case "time", "time_sec":
-			timeIndex = i
-		case "metric":
-			metricIndex = i
-		default:
-			if metricIndex == -1 {
-				switch columnTypes[i].DatabaseTypeName() {
-				case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT":
-					metricIndex = i
-				}
-			}
-		}
-	}
-
-	if timeIndex == -1 {
-		return fmt.Errorf("Found no column named time or time_sec")
-	}
-
-	fillMissing := query.Model.Get("fill").MustBool(false)
-	var fillInterval float64
-	fillValue := null.Float{}
-	if fillMissing {
-		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if !query.Model.Get("fillNull").MustBool(false) {
-			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
-			fillValue.Valid = true
-		}
-	}
-
-	for rows.Next() {
-		var timestamp float64
-		var value null.Float
-		var metric string
-
-		if rowCount > rowLimit {
-			return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit)
-		}
-
-		values, err := e.getTypedRowData(rows)
-		if err != nil {
-			return err
-		}
-
-		// converts column named time to unix timestamp in milliseconds to make
-		// native mysql datetime types and epoch dates work in
-		// annotation and table queries.
-		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
-		switch columnValue := values[timeIndex].(type) {
-		case int64:
-			timestamp = float64(columnValue)
-		case float64:
-			timestamp = columnValue
-		default:
-			return fmt.Errorf("Invalid type for column time/time_sec, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
-		}
-
-		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok {
-				metric = columnValue
-			} else {
-				return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
-			}
-		}
-
-		for i, col := range columnNames {
-			if i == timeIndex || i == metricIndex {
-				continue
-			}
-
-			if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
-				return err
-			}
-
-			if metricIndex == -1 {
-				metric = col
-			}
-
-			series, exist := pointsBySeries[metric]
-			if !exist {
-				series = &tsdb.TimeSeries{Name: metric}
-				pointsBySeries[metric] = series
-				seriesByQueryOrder.PushBack(metric)
-			}
-
-			if fillMissing {
-				var intervalStart float64
-				if !exist {
-					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
-				} else {
-					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
-				}
-
-				// align interval start
-				intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-
-				for i := intervalStart; i < timestamp; i += fillInterval {
-					series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
-					rowCount++
-				}
-			}
-
-			series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)})
-
-			e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
-			rowCount++
-
-		}
-	}
-
-	for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
-		key := elem.Value.(string)
-		result.Series = append(result.Series, pointsBySeries[key])
-
-		if fillMissing {
-			series := pointsBySeries[key]
-			// fill in values from last fetched value till interval end
-			intervalStart := series.Points[len(series.Points)-1][1].Float64
-			intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
-
-			// align interval start
-			intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-			for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
-				series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
-				rowCount++
-			}
-		}
-	}
-
-	result.Meta.Set("rowCount", rowCount)
-	return nil
-}
diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
index 850a37617e2..3b4e283b726 100644
--- a/pkg/tsdb/mysql/mysql_test.go
+++ b/pkg/tsdb/mysql/mysql_test.go
@@ -8,8 +8,9 @@ import (
 	"time"
 
 	"github.com/go-xorm/xorm"
+	"github.com/grafana/grafana/pkg/components/securejsondata"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/sqlstore"
 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
 	"github.com/grafana/grafana/pkg/tsdb"
@@ -21,8 +22,9 @@ import (
 // The tests require a MySQL db named grafana_ds_tests and a user/password grafana/password
 // Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a
 // preconfigured MySQL server suitable for running these tests.
-// There is also a dashboard.json in same directory that you can import to Grafana
-// once you've created a datasource for the test server/database.
+// There is also a datasource and dashboard provisioned by devenv scripts that you can
+// use to verify that the generated data are vizualized as expected, see
+// devenv/README.md for setup instructions.
 func TestMySQL(t *testing.T) {
 	// change to true to run the MySQL tests
 	runMySqlTests := false
@@ -35,19 +37,25 @@ func TestMySQL(t *testing.T) {
 	Convey("MySQL", t, func() {
 		x := InitMySQLTestDB(t)
 
-		endpoint := &MysqlQueryEndpoint{
-			sqlEngine: &tsdb.DefaultSqlEngine{
-				MacroEngine: NewMysqlMacroEngine(),
-				XormEngine:  x,
-			},
-			log: log.New("tsdb.mysql"),
+		origXormEngine := tsdb.NewXormEngine
+		tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) {
+			return x, nil
 		}
 
-		sess := x.NewSession()
-		defer sess.Close()
+		endpoint, err := newMysqlQueryEndpoint(&models.DataSource{
+			JsonData:       simplejson.New(),
+			SecureJsonData: securejsondata.SecureJsonData{},
+		})
+		So(err, ShouldBeNil)
 
+		sess := x.NewSession()
 		fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC)
 
+		Reset(func() {
+			sess.Close()
+			tsdb.NewXormEngine = origXormEngine
+		})
+
 		Convey("Given a table with different native data types", func() {
 			if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists {
 				So(err, ShouldBeNil)

From 4f7882cda2b3443e473caf426a321841b223a8ab Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 26 Jul 2018 18:11:10 +0200
Subject: [PATCH 108/380] mssql: use new sql engine

---
 pkg/tsdb/mssql/macros.go      |  38 ++---
 pkg/tsdb/mssql/macros_test.go |   2 +-
 pkg/tsdb/mssql/mssql.go       | 268 ++++------------------------------
 pkg/tsdb/mssql/mssql_test.go  |  30 ++--
 4 files changed, 64 insertions(+), 274 deletions(-)

diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index ad3d1edd5d7..2c16b5cb27f 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -14,18 +14,18 @@ import (
 const rsIdentifier = `([_a-zA-Z0-9]+)`
 const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)`
 
-type MsSqlMacroEngine struct {
-	TimeRange *tsdb.TimeRange
-	Query     *tsdb.Query
+type msSqlMacroEngine struct {
+	timeRange *tsdb.TimeRange
+	query     *tsdb.Query
 }
 
-func NewMssqlMacroEngine() tsdb.SqlMacroEngine {
-	return &MsSqlMacroEngine{}
+func newMssqlMacroEngine() tsdb.SqlMacroEngine {
+	return &msSqlMacroEngine{}
 }
 
-func (m *MsSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
-	m.TimeRange = timeRange
-	m.Query = query
+func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
+	m.timeRange = timeRange
+	m.query = query
 	rExp, _ := regexp.Compile(sExpr)
 	var macroError error
 
@@ -66,7 +66,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str
 	return result + str[lastIndex:]
 }
 
-func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
+func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
 	switch name {
 	case "__time":
 		if len(args) == 0 {
@@ -83,11 +83,11 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 
-		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeFrom":
-		return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeTo":
-		return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
+		return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
 	case "__timeGroup":
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval", name)
@@ -97,16 +97,16 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			return "", fmt.Errorf("error parsing interval %v", args[1])
 		}
 		if len(args) == 3 {
-			m.Query.Model.Set("fill", true)
-			m.Query.Model.Set("fillInterval", interval.Seconds())
+			m.query.Model.Set("fill", true)
+			m.query.Model.Set("fillInterval", interval.Seconds())
 			if args[2] == "NULL" {
-				m.Query.Model.Set("fillNull", true)
+				m.query.Model.Set("fillNull", true)
 			} else {
 				floatVal, err := strconv.ParseFloat(args[2], 64)
 				if err != nil {
 					return "", fmt.Errorf("error parsing fill value %v", args[2])
 				}
-				m.Query.Model.Set("fillValue", floatVal)
+				m.query.Model.Set("fillValue", floatVal)
 			}
 		}
 		return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
@@ -114,11 +114,11 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
-		return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
+		return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil
 	case "__unixEpochFrom":
-		return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
+		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
-		return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
+		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}
diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go
index 49368fe3631..1895cd99442 100644
--- a/pkg/tsdb/mssql/macros_test.go
+++ b/pkg/tsdb/mssql/macros_test.go
@@ -14,7 +14,7 @@ import (
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		engine := &MsSqlMacroEngine{}
+		engine := &msSqlMacroEngine{}
 		query := &tsdb.Query{
 			Model: simplejson.New(),
 		}
diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go
index eb71259b46b..72e57d03fa0 100644
--- a/pkg/tsdb/mssql/mssql.go
+++ b/pkg/tsdb/mssql/mssql.go
@@ -1,49 +1,40 @@
 package mssql
 
 import (
-	"container/list"
-	"context"
 	"database/sql"
 	"fmt"
 	"strconv"
 	"strings"
 
-	"math"
-
 	_ "github.com/denisenkom/go-mssqldb"
 	"github.com/go-xorm/core"
-	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
-type MssqlQueryEndpoint struct {
-	sqlEngine tsdb.SqlEngine
-	log       log.Logger
-}
-
 func init() {
-	tsdb.RegisterTsdbQueryEndpoint("mssql", NewMssqlQueryEndpoint)
+	tsdb.RegisterTsdbQueryEndpoint("mssql", newMssqlQueryEndpoint)
 }
 
-func NewMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
-	endpoint := &MssqlQueryEndpoint{
-		log: log.New("tsdb.mssql"),
-	}
-
-	endpoint.sqlEngine = &tsdb.DefaultSqlEngine{
-		MacroEngine: NewMssqlMacroEngine(),
-	}
+func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
+	logger := log.New("tsdb.mssql")
 
 	cnnstr := generateConnectionString(datasource)
-	endpoint.log.Debug("getEngine", "connection", cnnstr)
+	logger.Debug("getEngine", "connection", cnnstr)
 
-	if err := endpoint.sqlEngine.InitEngine("mssql", datasource, cnnstr); err != nil {
-		return nil, err
+	config := tsdb.SqlQueryEndpointConfiguration{
+		DriverName:        "mssql",
+		ConnectionString:  cnnstr,
+		Datasource:        datasource,
+		MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"},
 	}
 
-	return endpoint, nil
+	rowTransformer := mssqlRowTransformer{
+		log: logger,
+	}
+
+	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMssqlMacroEngine(), logger)
 }
 
 func generateConnectionString(datasource *models.DataSource) string {
@@ -70,71 +61,16 @@ func generateConnectionString(datasource *models.DataSource) string {
 	)
 }
 
-// Query is the main function for the MssqlQueryEndpoint
-func (e *MssqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
-	return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable)
+type mssqlRowTransformer struct {
+	log log.Logger
 }
 
-func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
-	columnNames, err := rows.Columns()
-	columnCount := len(columnNames)
+func (t *mssqlRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
+	values := make([]interface{}, len(columnTypes))
+	valuePtrs := make([]interface{}, len(columnTypes))
 
-	if err != nil {
-		return err
-	}
-
-	rowLimit := 1000000
-	rowCount := 0
-	timeIndex := -1
-
-	table := &tsdb.Table{
-		Columns: make([]tsdb.TableColumn, columnCount),
-		Rows:    make([]tsdb.RowValues, 0),
-	}
-
-	for i, name := range columnNames {
-		table.Columns[i].Text = name
-
-		// check if there is a column named time
-		switch name {
-		case "time":
-			timeIndex = i
-		}
-	}
-
-	columnTypes, err := rows.ColumnTypes()
-	if err != nil {
-		return err
-	}
-
-	for ; rows.Next(); rowCount++ {
-		if rowCount > rowLimit {
-			return fmt.Errorf("MsSQL query row limit exceeded, limit %d", rowLimit)
-		}
-
-		values, err := e.getTypedRowData(columnTypes, rows)
-		if err != nil {
-			return err
-		}
-
-		// converts column named time to unix timestamp in milliseconds
-		// to make native mssql datetime types and epoch dates work in
-		// annotation and table queries.
-		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-		table.Rows = append(table.Rows, values)
-	}
-
-	result.Tables = append(result.Tables, table)
-	result.Meta.Set("rowCount", rowCount)
-	return nil
-}
-
-func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
-	values := make([]interface{}, len(types))
-	valuePtrs := make([]interface{}, len(types))
-
-	for i, stype := range types {
-		e.log.Debug("type", "type", stype)
+	for i, stype := range columnTypes {
+		t.log.Debug("type", "type", stype)
 		valuePtrs[i] = &values[i]
 	}
 
@@ -144,17 +80,17 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
 
 	// convert types not handled by denisenkom/go-mssqldb
 	// unhandled types are returned as []byte
-	for i := 0; i < len(types); i++ {
+	for i := 0; i < len(columnTypes); i++ {
 		if value, ok := values[i].([]byte); ok {
-			switch types[i].DatabaseTypeName() {
+			switch columnTypes[i].DatabaseTypeName() {
 			case "MONEY", "SMALLMONEY", "DECIMAL":
 				if v, err := strconv.ParseFloat(string(value), 64); err == nil {
 					values[i] = v
 				} else {
-					e.log.Debug("Rows", "Error converting numeric to float", value)
+					t.log.Debug("Rows", "Error converting numeric to float", value)
 				}
 			default:
-				e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value)
+				t.log.Debug("Rows", "Unknown database type", columnTypes[i].DatabaseTypeName(), "value", value)
 				values[i] = string(value)
 			}
 		}
@@ -162,157 +98,3 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
 
 	return values, nil
 }
-
-func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
-	pointsBySeries := make(map[string]*tsdb.TimeSeries)
-	seriesByQueryOrder := list.New()
-
-	columnNames, err := rows.Columns()
-	if err != nil {
-		return err
-	}
-
-	columnTypes, err := rows.ColumnTypes()
-	if err != nil {
-		return err
-	}
-
-	rowLimit := 1000000
-	rowCount := 0
-	timeIndex := -1
-	metricIndex := -1
-
-	// check columns of resultset: a column named time is mandatory
-	// the first text column is treated as metric name unless a column named metric is present
-	for i, col := range columnNames {
-		switch col {
-		case "time":
-			timeIndex = i
-		case "metric":
-			metricIndex = i
-		default:
-			if metricIndex == -1 {
-				switch columnTypes[i].DatabaseTypeName() {
-				case "VARCHAR", "CHAR", "NVARCHAR", "NCHAR":
-					metricIndex = i
-				}
-			}
-		}
-	}
-
-	if timeIndex == -1 {
-		return fmt.Errorf("Found no column named time")
-	}
-
-	fillMissing := query.Model.Get("fill").MustBool(false)
-	var fillInterval float64
-	fillValue := null.Float{}
-	if fillMissing {
-		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if !query.Model.Get("fillNull").MustBool(false) {
-			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
-			fillValue.Valid = true
-		}
-	}
-
-	for rows.Next() {
-		var timestamp float64
-		var value null.Float
-		var metric string
-
-		if rowCount > rowLimit {
-			return fmt.Errorf("MSSQL query row limit exceeded, limit %d", rowLimit)
-		}
-
-		values, err := e.getTypedRowData(columnTypes, rows)
-		if err != nil {
-			return err
-		}
-
-		// converts column named time to unix timestamp in milliseconds to make
-		// native mysql datetime types and epoch dates work in
-		// annotation and table queries.
-		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
-
-		switch columnValue := values[timeIndex].(type) {
-		case int64:
-			timestamp = float64(columnValue)
-		case float64:
-			timestamp = columnValue
-		default:
-			return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
-		}
-
-		if metricIndex >= 0 {
-			if columnValue, ok := values[metricIndex].(string); ok {
-				metric = columnValue
-			} else {
-				return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
-			}
-		}
-
-		for i, col := range columnNames {
-			if i == timeIndex || i == metricIndex {
-				continue
-			}
-
-			if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
-				return err
-			}
-
-			if metricIndex == -1 {
-				metric = col
-			}
-
-			series, exist := pointsBySeries[metric]
-			if !exist {
-				series = &tsdb.TimeSeries{Name: metric}
-				pointsBySeries[metric] = series
-				seriesByQueryOrder.PushBack(metric)
-			}
-
-			if fillMissing {
-				var intervalStart float64
-				if !exist {
-					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
-				} else {
-					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
-				}
-
-				// align interval start
-				intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-
-				for i := intervalStart; i < timestamp; i += fillInterval {
-					series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
-					rowCount++
-				}
-			}
-
-			series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)})
-
-			e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
-		}
-	}
-
-	for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
-		key := elem.Value.(string)
-		result.Series = append(result.Series, pointsBySeries[key])
-
-		if fillMissing {
-			series := pointsBySeries[key]
-			// fill in values from last fetched value till interval end
-			intervalStart := series.Points[len(series.Points)-1][1].Float64
-			intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
-
-			// align interval start
-			intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
-			for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
-				series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
-				rowCount++
-			}
-		}
-	}
-
-	result.Meta.Set("rowCount", rowCount)
-	return nil
-}
diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go
index db04d6d1f02..86484cb9d5e 100644
--- a/pkg/tsdb/mssql/mssql_test.go
+++ b/pkg/tsdb/mssql/mssql_test.go
@@ -8,8 +8,9 @@ import (
 	"time"
 
 	"github.com/go-xorm/xorm"
+	"github.com/grafana/grafana/pkg/components/securejsondata"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
 	"github.com/grafana/grafana/pkg/tsdb"
 	. "github.com/smartystreets/goconvey/convey"
@@ -19,8 +20,9 @@ import (
 // The tests require a MSSQL db named grafanatest and a user/password grafana/Password!
 // Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a
 // preconfigured MSSQL server suitable for running these tests.
-// There is also a dashboard.json in same directory that you can import to Grafana
-// once you've created a datasource for the test server/database.
+// There is also a datasource and dashboard provisioned by devenv scripts that you can
+// use to verify that the generated data are vizualized as expected, see
+// devenv/README.md for setup instructions.
 // If needed, change the variable below to the IP address of the database.
 var serverIP = "localhost"
 
@@ -28,19 +30,25 @@ func TestMSSQL(t *testing.T) {
 	SkipConvey("MSSQL", t, func() {
 		x := InitMSSQLTestDB(t)
 
-		endpoint := &MssqlQueryEndpoint{
-			sqlEngine: &tsdb.DefaultSqlEngine{
-				MacroEngine: NewMssqlMacroEngine(),
-				XormEngine:  x,
-			},
-			log: log.New("tsdb.mssql"),
+		origXormEngine := tsdb.NewXormEngine
+		tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) {
+			return x, nil
 		}
 
-		sess := x.NewSession()
-		defer sess.Close()
+		endpoint, err := newMssqlQueryEndpoint(&models.DataSource{
+			JsonData:       simplejson.New(),
+			SecureJsonData: securejsondata.SecureJsonData{},
+		})
+		So(err, ShouldBeNil)
 
+		sess := x.NewSession()
 		fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
 
+		Reset(func() {
+			sess.Close()
+			tsdb.NewXormEngine = origXormEngine
+		})
+
 		Convey("Given a table with different native data types", func() {
 			sql := `
 					IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL

From 318b8c5a2346d60ede4fe2f01ffb0f665501709c Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 26 Jul 2018 18:12:00 +0200
Subject: [PATCH 109/380] update devenv datasources and dashboards for sql
 datasources

Removed dashboards from docker blocks
---
 devenv/datasources.yaml                       |  28 +++-
 .../datasource_tests_mssql_fakedata.json      |  79 ++++------
 .../datasource_tests_mssql_unittest.json      | 142 ++++++++----------
 .../datasource_tests_mysql_fakedata.json      |  68 +++------
 .../datasource_tests_mysql_unittest.json      | 136 ++++++++---------
 .../datasource_tests_postgres_fakedata.json   |  88 +++++------
 .../datasource_tests_postgres_unittest.json   | 142 ++++++++----------
 7 files changed, 306 insertions(+), 377 deletions(-)
 rename docker/blocks/mssql/dashboard.json => devenv/dev-dashboards/datasource_tests_mssql_fakedata.json (92%)
 rename docker/blocks/mssql_tests/dashboard.json => devenv/dev-dashboards/datasource_tests_mssql_unittest.json (96%)
 rename docker/blocks/mysql/dashboard.json => devenv/dev-dashboards/datasource_tests_mysql_fakedata.json (92%)
 rename docker/blocks/mysql_tests/dashboard.json => devenv/dev-dashboards/datasource_tests_mysql_unittest.json (96%)
 rename docker/blocks/postgres/dashboard.json => devenv/dev-dashboards/datasource_tests_postgres_fakedata.json (91%)
 rename docker/blocks/postgres_tests/dashboard.json => devenv/dev-dashboards/datasource_tests_postgres_unittest.json (95%)

diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml
index 241381097b1..a4e9bf05641 100644
--- a/devenv/datasources.yaml
+++ b/devenv/datasources.yaml
@@ -51,12 +51,28 @@ datasources:
     user: grafana
     password: password
 
+  - name: gdev-mysql-ds-tests
+    type: mysql
+    url: localhost:3306
+    database: grafana_ds_tests
+    user: grafana
+    password: password
+
   - name: gdev-mssql
     type: mssql
     url: localhost:1433
     database: grafana
     user: grafana
-    password: "Password!"
+    secureJsonData:
+      password: Password!
+
+  - name: gdev-mssql-ds-tests
+    type: mssql
+    url: localhost:1433
+    database: grafanatest
+    user: grafana
+    secureJsonData:
+      password: Password!
 
   - name: gdev-postgres
     type: postgres
@@ -68,6 +84,16 @@ datasources:
     jsonData:
       sslmode: "disable"
 
+  - name: gdev-postgres-ds-tests
+    type: postgres
+    url: localhost:5432
+    database: grafanadstest
+    user: grafanatest
+    secureJsonData:
+      password: grafanatest
+    jsonData:
+      sslmode: "disable"
+
   - name: gdev-cloudwatch
     type: cloudwatch
     editable: true
diff --git a/docker/blocks/mssql/dashboard.json b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
similarity index 92%
rename from docker/blocks/mssql/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
index ce9aa141a75..4350b5e44a8 100644
--- a/docker/blocks/mssql/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
@@ -1,40 +1,4 @@
 {
-  "__inputs": [
-    {
-      "name": "DS_MSSQL",
-      "label": "MSSQL",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "mssql",
-      "pluginName": "MSSQL"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": "5.0.0"
-    },
-    {
-      "type": "datasource",
-      "id": "mssql",
-      "name": "MSSQL",
-      "version": "1.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": "5.0.0"
-    }
-  ],
   "annotations": {
     "list": [
       {
@@ -52,8 +16,8 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1520976748896,
+  "id": 203,
+  "iteration": 1532618661457,
   "links": [],
   "panels": [
     {
@@ -63,7 +27,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL}",
+      "datasource": "gdev-mssql",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -149,14 +113,18 @@
           "min": null,
           "show": true
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     {
       "aliasColors": {},
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL}",
+      "datasource": "gdev-mssql",
       "fill": 2,
       "gridPos": {
         "h": 18,
@@ -234,14 +202,18 @@
           "min": null,
           "show": true
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     {
       "aliasColors": {},
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL}",
+      "datasource": "gdev-mssql",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -313,11 +285,15 @@
           "min": null,
           "show": true
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     {
       "columns": [],
-      "datasource": "${DS_MSSQL}",
+      "datasource": "gdev-mssql",
       "fontSize": "100%",
       "gridPos": {
         "h": 10,
@@ -371,13 +347,13 @@
   ],
   "schemaVersion": 16,
   "style": "dark",
-  "tags": [],
+  "tags": ["gdev", "mssql", "fake-data-gen"],
   "templating": {
     "list": [
       {
         "allValue": null,
         "current": {},
-        "datasource": "${DS_MSSQL}",
+        "datasource": "gdev-mssql",
         "hide": 0,
         "includeAll": false,
         "label": "Datacenter",
@@ -387,6 +363,7 @@
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "refresh": 1,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "tagValuesQuery": "",
         "tags": [],
@@ -397,7 +374,7 @@
       {
         "allValue": null,
         "current": {},
-        "datasource": "${DS_MSSQL}",
+        "datasource": "gdev-mssql",
         "hide": 0,
         "includeAll": true,
         "label": "Hostname",
@@ -407,6 +384,7 @@
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "refresh": 1,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "tagValuesQuery": "",
         "tags": [],
@@ -499,6 +477,7 @@
         ],
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
       }
     ]
@@ -533,7 +512,7 @@
     ]
   },
   "timezone": "",
-  "title": "Grafana Fake Data Gen - MSSQL",
+  "title": "Datasource tests - MSSQL",
   "uid": "86Js1xRmk",
-  "version": 11
+  "version": 1
 }
\ No newline at end of file
diff --git a/docker/blocks/mssql_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
similarity index 96%
rename from docker/blocks/mssql_tests/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_mssql_unittest.json
index 80994254093..5c8eb8243a3 100644
--- a/docker/blocks/mssql_tests/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
@@ -1,40 +1,4 @@
 {
-  "__inputs": [
-    {
-      "name": "DS_MSSQL_TEST",
-      "label": "MSSQL Test",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "mssql",
-      "pluginName": "Microsoft SQL Server"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": "5.0.0"
-    },
-    {
-      "type": "datasource",
-      "id": "mssql",
-      "name": "Microsoft SQL Server",
-      "version": "1.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": "5.0.0"
-    }
-  ],
   "annotations": {
     "list": [
       {
@@ -47,7 +11,7 @@
         "type": "dashboard"
       },
       {
-        "datasource": "${DS_MSSQL_TEST}",
+        "datasource": "gdev-mssql-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#6ed0e0",
@@ -59,7 +23,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_MSSQL_TEST}",
+        "datasource": "gdev-mssql-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "rgba(255, 96, 96, 1)",
@@ -71,7 +35,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_MSSQL_TEST}",
+        "datasource": "gdev-mssql-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#7eb26d",
@@ -83,7 +47,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_MSSQL_TEST}",
+        "datasource": "gdev-mssql-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#1f78c1",
@@ -96,16 +60,17 @@
       }
     ]
   },
+  "description": "Run the mssql unit tests to generate the data backing this dashboard",
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1523320861623,
+  "id": 35,
+  "iteration": 1532618879985,
   "links": [],
   "panels": [
     {
       "columns": [],
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 4,
@@ -152,7 +117,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -206,7 +171,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -260,7 +225,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -314,7 +279,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -371,7 +336,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -454,7 +419,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -537,7 +502,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -620,7 +585,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -703,7 +668,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -786,7 +751,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -869,7 +834,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -962,7 +927,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1065,7 +1030,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1158,7 +1123,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1243,7 +1208,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1336,7 +1301,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1421,7 +1386,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1514,7 +1479,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1599,7 +1564,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1686,7 +1651,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1773,7 +1738,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1867,7 +1832,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1954,7 +1919,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2048,7 +2013,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2135,7 +2100,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2229,7 +2194,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2316,7 +2281,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2410,7 +2375,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MSSQL_TEST}",
+      "datasource": "gdev-mssql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2496,22 +2461,44 @@
   "refresh": false,
   "schemaVersion": 16,
   "style": "dark",
-  "tags": [],
+  "tags": ["gdev", "mssql"],
   "templating": {
     "list": [
       {
         "allValue": "'ALL'",
-        "current": {},
-        "datasource": "${DS_MSSQL_TEST}",
+        "current": {
+          "selected": true,
+          "tags": [],
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "gdev-mssql-ds-tests",
         "hide": 0,
         "includeAll": true,
         "label": "Metric",
         "multi": false,
         "name": "metric",
-        "options": [],
+        "options": [
+          {
+            "selected": true,
+            "text": "All",
+            "value": "$__all"
+          },
+          {
+            "selected": false,
+            "text": "Metric A",
+            "value": "Metric A"
+          },
+          {
+            "selected": false,
+            "text": "Metric B",
+            "value": "Metric B"
+          }
+        ],
         "query": "SELECT DISTINCT measurement FROM metric_values",
-        "refresh": 1,
+        "refresh": 0,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 0,
         "tagValuesQuery": "",
         "tags": [],
@@ -2564,6 +2551,7 @@
         ],
         "query": "1s,10s,30s,1m,5m,10m",
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
       }
     ]
@@ -2598,7 +2586,7 @@
     ]
   },
   "timezone": "",
-  "title": "Microsoft SQL Server Data Source Test",
+  "title": "Datasource tests - MSSQL (unit test)",
   "uid": "GlAqcPgmz",
   "version": 58
 }
\ No newline at end of file
diff --git a/docker/blocks/mysql/dashboard.json b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
similarity index 92%
rename from docker/blocks/mysql/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
index dba7847cc72..cef8fd4783f 100644
--- a/docker/blocks/mysql/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
@@ -1,40 +1,4 @@
 {
-  "__inputs": [
-    {
-      "name": "DS_MYSQL",
-      "label": "MySQL",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "mysql",
-      "pluginName": "MySQL"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": "5.0.0"
-    },
-    {
-      "type": "datasource",
-      "id": "mysql",
-      "name": "MySQL",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": "5.0.0"
-    }
-  ],
   "annotations": {
     "list": [
       {
@@ -52,8 +16,8 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1523372133566,
+  "id": 4,
+  "iteration": 1532620738041,
   "links": [],
   "panels": [
     {
@@ -63,7 +27,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL}",
+      "datasource": "gdev-mysql",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -161,7 +125,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL}",
+      "datasource": "gdev-mysql",
       "fill": 2,
       "gridPos": {
         "h": 18,
@@ -251,7 +215,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL}",
+      "datasource": "gdev-mysql",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -332,7 +296,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MYSQL}",
+      "datasource": "gdev-mysql",
       "fontSize": "100%",
       "gridPos": {
         "h": 9,
@@ -390,6 +354,7 @@
   "schemaVersion": 16,
   "style": "dark",
   "tags": [
+    "gdev",
     "fake-data-gen",
     "mysql"
   ],
@@ -397,8 +362,11 @@
     "list": [
       {
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_MYSQL}",
+        "current": {
+          "text": "America",
+          "value": "America"
+        },
+        "datasource": "gdev-mysql",
         "hide": 0,
         "includeAll": false,
         "label": "Datacenter",
@@ -408,6 +376,7 @@
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "refresh": 1,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "tagValuesQuery": "",
         "tags": [],
@@ -417,8 +386,11 @@
       },
       {
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_MYSQL}",
+        "current": {
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "gdev-mysql",
         "hide": 0,
         "includeAll": true,
         "label": "Hostname",
@@ -428,6 +400,7 @@
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "refresh": 1,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "tagValuesQuery": "",
         "tags": [],
@@ -520,6 +493,7 @@
         ],
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
       }
     ]
@@ -554,7 +528,7 @@
     ]
   },
   "timezone": "",
-  "title": "Grafana Fake Data Gen - MySQL",
+  "title": "Datasource tests - MySQL",
   "uid": "DGsCac3kz",
   "version": 8
 }
\ No newline at end of file
diff --git a/docker/blocks/mysql_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
similarity index 96%
rename from docker/blocks/mysql_tests/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_mysql_unittest.json
index 53f313315bd..2c20969da12 100644
--- a/docker/blocks/mysql_tests/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
@@ -1,40 +1,4 @@
 {
-  "__inputs": [
-    {
-      "name": "DS_MYSQL_TEST",
-      "label": "MySQL TEST",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "mysql",
-      "pluginName": "MySQL"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": "5.0.0"
-    },
-    {
-      "type": "datasource",
-      "id": "mysql",
-      "name": "MySQL",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": "5.0.0"
-    }
-  ],
   "annotations": {
     "list": [
       {
@@ -47,7 +11,7 @@
         "type": "dashboard"
       },
       {
-        "datasource": "${DS_MYSQL_TEST}",
+        "datasource": "gdev-mysql-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#6ed0e0",
@@ -59,7 +23,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_MYSQL_TEST}",
+        "datasource": "gdev-mysql-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "rgba(255, 96, 96, 1)",
@@ -71,7 +35,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_MYSQL_TEST}",
+        "datasource": "gdev-mysql-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#7eb26d",
@@ -83,7 +47,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_MYSQL_TEST}",
+        "datasource": "gdev-mysql-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#1f78c1",
@@ -96,16 +60,17 @@
       }
     ]
   },
+  "description": "Run the mysql unit tests to generate the data backing this dashboard",
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1523320712115,
+  "id": 39,
+  "iteration": 1532620354037,
   "links": [],
   "panels": [
     {
       "columns": [],
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 4,
@@ -152,7 +117,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -206,7 +171,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -260,7 +225,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -314,7 +279,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -371,7 +336,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -454,7 +419,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -537,7 +502,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -620,7 +585,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -703,7 +668,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -786,7 +751,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -869,7 +834,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -962,7 +927,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1059,7 +1024,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1152,7 +1117,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1237,7 +1202,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1330,7 +1295,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1415,7 +1380,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1508,7 +1473,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1593,7 +1558,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1687,7 +1652,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1774,7 +1739,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1868,7 +1833,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1955,7 +1920,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2049,7 +2014,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2136,7 +2101,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2230,7 +2195,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_MYSQL_TEST}",
+      "datasource": "gdev-mysql-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2316,22 +2281,42 @@
   "refresh": false,
   "schemaVersion": 16,
   "style": "dark",
-  "tags": [],
+  "tags": ["gdev", "mysql"],
   "templating": {
     "list": [
       {
         "allValue": "",
-        "current": {},
-        "datasource": "${DS_MYSQL_TEST}",
+        "current": {
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "gdev-mysql-ds-tests",
         "hide": 0,
         "includeAll": true,
         "label": "Metric",
         "multi": true,
         "name": "metric",
-        "options": [],
+        "options": [
+          {
+            "selected": true,
+            "text": "All",
+            "value": "$__all"
+          },
+          {
+            "selected": false,
+            "text": "Metric A",
+            "value": "Metric A"
+          },
+          {
+            "selected": false,
+            "text": "Metric B",
+            "value": "Metric B"
+          }
+        ],
         "query": "SELECT DISTINCT measurement FROM metric_values",
-        "refresh": 1,
+        "refresh": 0,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 0,
         "tagValuesQuery": "",
         "tags": [],
@@ -2384,6 +2369,7 @@
         ],
         "query": "1s,10s,30s,1m,5m,10m",
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
       }
     ]
@@ -2418,7 +2404,7 @@
     ]
   },
   "timezone": "",
-  "title": "MySQL Data Source Test",
+  "title": "Datasource tests - MySQL (unittest)",
   "uid": "Hmf8FDkmz",
   "version": 12
 }
\ No newline at end of file
diff --git a/docker/blocks/postgres/dashboard.json b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
similarity index 91%
rename from docker/blocks/postgres/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
index 77b0ceac624..1afa6e25df8 100644
--- a/docker/blocks/postgres/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
@@ -1,40 +1,4 @@
 {
-  "__inputs": [
-    {
-      "name": "DS_POSTGRESQL",
-      "label": "PostgreSQL",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "postgres",
-      "pluginName": "PostgreSQL"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": ""
-    },
-    {
-      "type": "datasource",
-      "id": "postgres",
-      "name": "PostgreSQL",
-      "version": "1.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": ""
-    }
-  ],
   "annotations": {
     "list": [
       {
@@ -52,8 +16,8 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1518601837383,
+  "id": 5,
+  "iteration": 1532620601931,
   "links": [],
   "panels": [
     {
@@ -63,7 +27,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRESQL}",
+      "datasource": "gdev-postgres",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -150,14 +114,18 @@
           "min": null,
           "show": true
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     {
       "aliasColors": {},
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRESQL}",
+      "datasource": "gdev-postgres",
       "fill": 2,
       "gridPos": {
         "h": 18,
@@ -236,14 +204,18 @@
           "min": null,
           "show": true
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     {
       "aliasColors": {},
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRESQL}",
+      "datasource": "gdev-postgres",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -316,11 +288,15 @@
           "min": null,
           "show": true
         }
-      ]
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     {
       "columns": [],
-      "datasource": "${DS_POSTGRESQL}",
+      "datasource": "gdev-postgres",
       "fontSize": "100%",
       "gridPos": {
         "h": 9,
@@ -377,6 +353,7 @@
   "schemaVersion": 16,
   "style": "dark",
   "tags": [
+    "gdev",
     "fake-data-gen",
     "postgres"
   ],
@@ -384,8 +361,11 @@
     "list": [
       {
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_POSTGRESQL}",
+        "current": {
+          "text": "America",
+          "value": "America"
+        },
+        "datasource": "gdev-postgres",
         "hide": 0,
         "includeAll": false,
         "label": "Datacenter",
@@ -395,6 +375,7 @@
         "query": "SELECT DISTINCT datacenter FROM grafana_metric",
         "refresh": 1,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "tagValuesQuery": "",
         "tags": [],
@@ -404,8 +385,11 @@
       },
       {
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_POSTGRESQL}",
+        "current": {
+          "text": "All",
+          "value": "$__all"
+        },
+        "datasource": "gdev-postgres",
         "hide": 0,
         "includeAll": true,
         "label": "Hostname",
@@ -415,6 +399,7 @@
         "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'",
         "refresh": 1,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "tagValuesQuery": "",
         "tags": [],
@@ -507,6 +492,7 @@
         ],
         "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d",
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
       }
     ]
@@ -541,7 +527,7 @@
     ]
   },
   "timezone": "",
-  "title": "Grafana Fake Data Gen - PostgreSQL",
+  "title": "Datasource tests - Postgres",
   "uid": "JYola5qzz",
-  "version": 1
+  "version": 4
 }
\ No newline at end of file
diff --git a/docker/blocks/postgres_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
similarity index 95%
rename from docker/blocks/postgres_tests/dashboard.json
rename to devenv/dev-dashboards/datasource_tests_postgres_unittest.json
index 9efbe90bdfe..d7d5f238e85 100644
--- a/docker/blocks/postgres_tests/dashboard.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
@@ -1,40 +1,4 @@
 {
-  "__inputs": [
-    {
-      "name": "DS_POSTGRES_TEST",
-      "label": "Postgres TEST",
-      "description": "",
-      "type": "datasource",
-      "pluginId": "postgres",
-      "pluginName": "PostgreSQL"
-    }
-  ],
-  "__requires": [
-    {
-      "type": "grafana",
-      "id": "grafana",
-      "name": "Grafana",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "graph",
-      "name": "Graph",
-      "version": "5.0.0"
-    },
-    {
-      "type": "datasource",
-      "id": "postgres",
-      "name": "PostgreSQL",
-      "version": "5.0.0"
-    },
-    {
-      "type": "panel",
-      "id": "table",
-      "name": "Table",
-      "version": "5.0.0"
-    }
-  ],
   "annotations": {
     "list": [
       {
@@ -47,7 +11,7 @@
         "type": "dashboard"
       },
       {
-        "datasource": "${DS_POSTGRES_TEST}",
+        "datasource": "gdev-postgres-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#6ed0e0",
@@ -59,7 +23,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_POSTGRES_TEST}",
+        "datasource": "gdev-postgres-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "rgba(255, 96, 96, 1)",
@@ -71,7 +35,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_POSTGRES_TEST}",
+        "datasource": "gdev-postgres-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#7eb26d",
@@ -83,7 +47,7 @@
         "type": "tags"
       },
       {
-        "datasource": "${DS_POSTGRES_TEST}",
+        "datasource": "gdev-postgres-ds-tests",
         "enable": false,
         "hide": false,
         "iconColor": "#1f78c1",
@@ -96,16 +60,17 @@
       }
     ]
   },
+  "description": "Run the postgres unit tests to generate the data backing this dashboard",
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": null,
-  "iteration": 1523320929325,
+  "id": 38,
+  "iteration": 1532619575136,
   "links": [],
   "panels": [
     {
       "columns": [],
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 4,
@@ -152,7 +117,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -206,7 +171,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -260,7 +225,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -314,7 +279,7 @@
     },
     {
       "columns": [],
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fontSize": "100%",
       "gridPos": {
         "h": 3,
@@ -371,7 +336,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -454,7 +419,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -537,7 +502,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -620,7 +585,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -703,7 +668,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -786,7 +751,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 9,
@@ -869,7 +834,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -962,7 +927,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1047,7 +1012,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1140,7 +1105,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1225,7 +1190,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1318,7 +1283,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1403,7 +1368,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1496,7 +1461,7 @@
       "bars": false,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
         "h": 8,
@@ -1581,7 +1546,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1675,7 +1640,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1762,7 +1727,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1856,7 +1821,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -1943,7 +1908,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2037,7 +2002,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2124,7 +2089,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2218,7 +2183,7 @@
       "bars": true,
       "dashLength": 10,
       "dashes": false,
-      "datasource": "${DS_POSTGRES_TEST}",
+      "datasource": "gdev-postgres-ds-tests",
       "fill": 1,
       "gridPos": {
         "h": 8,
@@ -2304,22 +2269,46 @@
   "refresh": false,
   "schemaVersion": 16,
   "style": "dark",
-  "tags": [],
+  "tags": ["gdev", "postgres"],
   "templating": {
     "list": [
       {
         "allValue": null,
-        "current": {},
-        "datasource": "${DS_POSTGRES_TEST}",
+        "current": {
+          "selected": true,
+          "tags": [],
+          "text": "All",
+          "value": [
+            "$__all"
+          ]
+        },
+        "datasource": "gdev-postgres-ds-tests",
         "hide": 0,
         "includeAll": true,
         "label": "Metric",
         "multi": true,
         "name": "metric",
-        "options": [],
+        "options": [
+          {
+            "selected": true,
+            "text": "All",
+            "value": "$__all"
+          },
+          {
+            "selected": false,
+            "text": "Metric A",
+            "value": "Metric A"
+          },
+          {
+            "selected": false,
+            "text": "Metric B",
+            "value": "Metric B"
+          }
+        ],
         "query": "SELECT DISTINCT measurement FROM metric_values",
-        "refresh": 1,
+        "refresh": 0,
         "regex": "",
+        "skipUrlSync": false,
         "sort": 1,
         "tagValuesQuery": "",
         "tags": [],
@@ -2372,6 +2361,7 @@
         ],
         "query": "1s,10s,30s,1m,5m,10m",
         "refresh": 2,
+        "skipUrlSync": false,
         "type": "interval"
       }
     ]
@@ -2406,7 +2396,7 @@
     ]
   },
   "timezone": "",
-  "title": "Postgres Data Source Test",
+  "title": "Datasource tests - Postgres (unittest)",
   "uid": "vHQdlVziz",
-  "version": 14
+  "version": 17
 }
\ No newline at end of file

From ab8fa0de7443136afeab82fcf8713fddbdc23a48 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 26 Jul 2018 21:39:02 +0200
Subject: [PATCH 110/380] elasticsearch: support reversed index patterns

Now both [index-]pattern and pattern[-index] are supported
---
 .../elasticsearch/client/index_pattern.go     | 35 ++++++++++++++-----
 .../client/index_pattern_test.go              | 27 +++++++++++++-
 2 files changed, 53 insertions(+), 9 deletions(-)

diff --git a/pkg/tsdb/elasticsearch/client/index_pattern.go b/pkg/tsdb/elasticsearch/client/index_pattern.go
index 8391e902ea4..952b5c4f806 100644
--- a/pkg/tsdb/elasticsearch/client/index_pattern.go
+++ b/pkg/tsdb/elasticsearch/client/index_pattern.go
@@ -248,13 +248,28 @@ var datePatternReplacements = map[string]string{
 
 func formatDate(t time.Time, pattern string) string {
 	var datePattern string
-	parts := strings.Split(strings.TrimLeft(pattern, "["), "]")
-	base := parts[0]
-	if len(parts) == 2 {
-		datePattern = parts[1]
-	} else {
-		datePattern = base
-		base = ""
+	base := ""
+	ltr := false
+
+	if strings.HasPrefix(pattern, "[") {
+		parts := strings.Split(strings.TrimLeft(pattern, "["), "]")
+		base = parts[0]
+		if len(parts) == 2 {
+			datePattern = parts[1]
+		} else {
+			datePattern = base
+			base = ""
+		}
+		ltr = true
+	} else if strings.HasSuffix(pattern, "]") {
+		parts := strings.Split(strings.TrimRight(pattern, "]"), "[")
+		datePattern = parts[0]
+		if len(parts) == 2 {
+			base = parts[1]
+		} else {
+			base = ""
+		}
+		ltr = false
 	}
 
 	formatted := t.Format(patternToLayout(datePattern))
@@ -293,7 +308,11 @@ func formatDate(t time.Time, pattern string) string {
 		formatted = strings.Replace(formatted, "<stdHourNoZero>", fmt.Sprintf("%d", t.Hour()), -1)
 	}
 
-	return base + formatted
+	if ltr {
+		return base + formatted
+	}
+
+	return formatted + base
 }
 
 func patternToLayout(pattern string) string {
diff --git a/pkg/tsdb/elasticsearch/client/index_pattern_test.go b/pkg/tsdb/elasticsearch/client/index_pattern_test.go
index 3bd823d8c87..ca20b39d532 100644
--- a/pkg/tsdb/elasticsearch/client/index_pattern_test.go
+++ b/pkg/tsdb/elasticsearch/client/index_pattern_test.go
@@ -28,29 +28,54 @@ func TestIndexPattern(t *testing.T) {
 		to := fmt.Sprintf("%d", time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond))
 
 		indexPatternScenario(intervalHourly, "[data-]YYYY.MM.DD.HH", tsdb.NewTimeRange(from, to), func(indices []string) {
-			//So(indices, ShouldHaveLength, 1)
+			So(indices, ShouldHaveLength, 1)
 			So(indices[0], ShouldEqual, "data-2018.05.15.17")
 		})
 
+		indexPatternScenario(intervalHourly, "YYYY.MM.DD.HH[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+			So(indices, ShouldHaveLength, 1)
+			So(indices[0], ShouldEqual, "2018.05.15.17-data")
+		})
+
 		indexPatternScenario(intervalDaily, "[data-]YYYY.MM.DD", tsdb.NewTimeRange(from, to), func(indices []string) {
 			So(indices, ShouldHaveLength, 1)
 			So(indices[0], ShouldEqual, "data-2018.05.15")
 		})
 
+		indexPatternScenario(intervalDaily, "YYYY.MM.DD[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+			So(indices, ShouldHaveLength, 1)
+			So(indices[0], ShouldEqual, "2018.05.15-data")
+		})
+
 		indexPatternScenario(intervalWeekly, "[data-]GGGG.WW", tsdb.NewTimeRange(from, to), func(indices []string) {
 			So(indices, ShouldHaveLength, 1)
 			So(indices[0], ShouldEqual, "data-2018.20")
 		})
 
+		indexPatternScenario(intervalWeekly, "GGGG.WW[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+			So(indices, ShouldHaveLength, 1)
+			So(indices[0], ShouldEqual, "2018.20-data")
+		})
+
 		indexPatternScenario(intervalMonthly, "[data-]YYYY.MM", tsdb.NewTimeRange(from, to), func(indices []string) {
 			So(indices, ShouldHaveLength, 1)
 			So(indices[0], ShouldEqual, "data-2018.05")
 		})
 
+		indexPatternScenario(intervalMonthly, "YYYY.MM[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+			So(indices, ShouldHaveLength, 1)
+			So(indices[0], ShouldEqual, "2018.05-data")
+		})
+
 		indexPatternScenario(intervalYearly, "[data-]YYYY", tsdb.NewTimeRange(from, to), func(indices []string) {
 			So(indices, ShouldHaveLength, 1)
 			So(indices[0], ShouldEqual, "data-2018")
 		})
+
+		indexPatternScenario(intervalYearly, "YYYY[-data]", tsdb.NewTimeRange(from, to), func(indices []string) {
+			So(indices, ShouldHaveLength, 1)
+			So(indices[0], ShouldEqual, "2018-data")
+		})
 	})
 
 	Convey("Hourly interval", t, func() {

From 48e5e65c73eea000bf2b702b8743de0146e29f86 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 27 Jul 2018 10:33:06 +0200
Subject: [PATCH 111/380] changelog: add notes about closing #12731

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6409f094f65..ad1b63234e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
 * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
+* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 
 # 5.2.2 (2018-07-25)
 

From 675a031b6c9c367fe27de5e839c1d919ca09021d Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 11:04:01 +0200
Subject: [PATCH 112/380] All except one passing

---
 public/app/plugins/panel/singlestat/module.ts                | 5 ++++-
 public/app/plugins/panel/singlestat/specs/singlestat.jest.ts | 4 ++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts
index 7fafb5902d1..b63182141c1 100644
--- a/public/app/plugins/panel/singlestat/module.ts
+++ b/public/app/plugins/panel/singlestat/module.ts
@@ -310,11 +310,14 @@ class SingleStatCtrl extends MetricsPanelCtrl {
         data.valueRounded = data.value;
         data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc());
       } else {
-        console.log(lastPoint, lastValue);
+        // console.log(lastPoint, lastValue);
+        // console.log(this.panel.valueName);
+        // console.log(this.panel);
         data.value = this.series[0].stats[this.panel.valueName];
         data.flotpairs = this.series[0].flotpairs;
 
         let decimalInfo = this.getDecimalsForValue(data.value);
+        console.log(decimalInfo);
         let formatFunc = kbn.valueFormats[this.panel.format];
         data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
         data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
index 7b89f86250c..798298415a9 100644
--- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
+++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
@@ -192,6 +192,8 @@ describe('SingleStatCtrl', function() {
   ) {
     ctx.setup(function() {
       ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }];
+      ctx.ctrl.panel.valueName = 'avg';
+      ctx.ctrl.panel.format = 'none';
     });
 
     it('Should be rounded', function() {
@@ -259,7 +261,9 @@ describe('SingleStatCtrl', function() {
     singleStatScenario('with default values', function(ctx) {
       ctx.setup(function() {
         ctx.data = tableData;
+        ctx.ctrl.panel = {};
         ctx.ctrl.panel.tableColumn = 'mean';
+        ctx.ctrl.panel.format = 'none';
       });
 
       it('Should use first rows value as default main value', function() {

From 47da3e3ae83f36207cedfa26e9b5d51ca21b112f Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 11:28:16 +0200
Subject: [PATCH 113/380] All tests passing

---
 public/app/plugins/panel/singlestat/module.ts                | 4 ----
 public/app/plugins/panel/singlestat/specs/singlestat.jest.ts | 2 ++
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts
index b63182141c1..ebd2628b086 100644
--- a/public/app/plugins/panel/singlestat/module.ts
+++ b/public/app/plugins/panel/singlestat/module.ts
@@ -310,14 +310,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
         data.valueRounded = data.value;
         data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc());
       } else {
-        // console.log(lastPoint, lastValue);
-        // console.log(this.panel.valueName);
-        // console.log(this.panel);
         data.value = this.series[0].stats[this.panel.valueName];
         data.flotpairs = this.series[0].flotpairs;
 
         let decimalInfo = this.getDecimalsForValue(data.value);
-        console.log(decimalInfo);
         let formatFunc = kbn.valueFormats[this.panel.format];
         data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
         data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
index 798298415a9..552ac2412d6 100644
--- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
+++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
@@ -293,6 +293,7 @@ describe('SingleStatCtrl', function() {
       ctx.setup(function() {
         ctx.data = tableData;
         ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2'];
+        ctx.ctrl.panel.mappingType = 0;
         ctx.ctrl.panel.tableColumn = 'mean';
       });
 
@@ -310,6 +311,7 @@ describe('SingleStatCtrl', function() {
       ctx.setup(function() {
         ctx.data = tableData;
         ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2'];
+        ctx.ctrl.panel.mappingType = 2;
         ctx.ctrl.panel.tableColumn = 'mean';
         ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
       });

From 3d21e42aac715c28fe3325bd3ce9f7a00cb39312 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 11:30:37 +0200
Subject: [PATCH 114/380] Remove Karma file

---
 .../singlestat/specs/singlestat_specs.ts      | 362 ------------------
 1 file changed, 362 deletions(-)
 delete mode 100644 public/app/plugins/panel/singlestat/specs/singlestat_specs.ts

diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts b/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts
deleted file mode 100644
index 217ec5ee04c..00000000000
--- a/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts
+++ /dev/null
@@ -1,362 +0,0 @@
-import { describe, beforeEach, afterEach, it, sinon, expect, angularMocks } from 'test/lib/common';
-
-import helpers from 'test/specs/helpers';
-import { SingleStatCtrl } from '../module';
-import moment from 'moment';
-
-describe('SingleStatCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
-  var epoch = 1505826363746;
-  var clock;
-
-  function singleStatScenario(desc, func) {
-    describe(desc, function() {
-      ctx.setup = function(setupFunc) {
-        beforeEach(angularMocks.module('grafana.services'));
-        beforeEach(angularMocks.module('grafana.controllers'));
-        beforeEach(
-          angularMocks.module(function($compileProvider) {
-            $compileProvider.preAssignBindingsEnabled(true);
-          })
-        );
-
-        beforeEach(ctx.providePhase());
-        beforeEach(ctx.createPanelController(SingleStatCtrl));
-
-        beforeEach(function() {
-          setupFunc();
-          ctx.ctrl.onDataReceived(ctx.data);
-          ctx.data = ctx.ctrl.data;
-        });
-      };
-
-      func(ctx);
-    });
-  }
-
-  singleStatScenario('with defaults', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
-    });
-
-    it('Should use series avg as default main value', function() {
-      expect(ctx.data.value).to.be(15);
-      expect(ctx.data.valueRounded).to.be(15);
-    });
-
-    it('should set formatted falue', function() {
-      expect(ctx.data.valueFormatted).to.be('15');
-    });
-  });
-
-  singleStatScenario('showing serie name instead of value', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
-      ctx.ctrl.panel.valueName = 'name';
-    });
-
-    it('Should use series avg as default main value', function() {
-      expect(ctx.data.value).to.be(0);
-      expect(ctx.data.valueRounded).to.be(0);
-    });
-
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).to.be('test.cpu1');
-    });
-  });
-
-  singleStatScenario('showing last iso time instead of value', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
-      ctx.ctrl.panel.valueName = 'last_time';
-      ctx.ctrl.panel.format = 'dateTimeAsIso';
-    });
-
-    it('Should use time instead of value', function() {
-      expect(ctx.data.value).to.be(1505634997920);
-      expect(ctx.data.valueRounded).to.be(1505634997920);
-    });
-
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).to.be(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss'));
-    });
-  });
-
-  singleStatScenario('showing last iso time instead of value (in UTC)', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
-      ctx.ctrl.panel.valueName = 'last_time';
-      ctx.ctrl.panel.format = 'dateTimeAsIso';
-      ctx.setIsUtc(true);
-    });
-
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).to.be(moment.utc(1505634997920).format('YYYY-MM-DD HH:mm:ss'));
-    });
-  });
-
-  singleStatScenario('showing last us time instead of value', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
-      ctx.ctrl.panel.valueName = 'last_time';
-      ctx.ctrl.panel.format = 'dateTimeAsUS';
-    });
-
-    it('Should use time instead of value', function() {
-      expect(ctx.data.value).to.be(1505634997920);
-      expect(ctx.data.valueRounded).to.be(1505634997920);
-    });
-
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).to.be(moment(1505634997920).format('MM/DD/YYYY h:mm:ss a'));
-    });
-  });
-
-  singleStatScenario('showing last us time instead of value (in UTC)', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
-      ctx.ctrl.panel.valueName = 'last_time';
-      ctx.ctrl.panel.format = 'dateTimeAsUS';
-      ctx.setIsUtc(true);
-    });
-
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).to.be(moment.utc(1505634997920).format('MM/DD/YYYY h:mm:ss a'));
-    });
-  });
-
-  singleStatScenario('showing last time from now instead of value', function(ctx) {
-    beforeEach(() => {
-      clock = sinon.useFakeTimers(epoch);
-    });
-
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
-      ctx.ctrl.panel.valueName = 'last_time';
-      ctx.ctrl.panel.format = 'dateTimeFromNow';
-    });
-
-    it('Should use time instead of value', function() {
-      expect(ctx.data.value).to.be(1505634997920);
-      expect(ctx.data.valueRounded).to.be(1505634997920);
-    });
-
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).to.be('2 days ago');
-    });
-
-    afterEach(() => {
-      clock.restore();
-    });
-  });
-
-  singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) {
-    beforeEach(() => {
-      clock = sinon.useFakeTimers(epoch);
-    });
-
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
-      ctx.ctrl.panel.valueName = 'last_time';
-      ctx.ctrl.panel.format = 'dateTimeFromNow';
-      ctx.setIsUtc(true);
-    });
-
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).to.be('2 days ago');
-    });
-
-    afterEach(() => {
-      clock.restore();
-    });
-  });
-
-  singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function(
-    ctx
-  ) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }];
-    });
-
-    it('Should be rounded', function() {
-      expect(ctx.data.value).to.be(99.999495);
-      expect(ctx.data.valueRounded).to.be(100);
-    });
-
-    it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).to.be('100');
-    });
-  });
-
-  singleStatScenario('When value to text mapping is specified', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }];
-      ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
-    });
-
-    it('value should remain', function() {
-      expect(ctx.data.value).to.be(9.9);
-    });
-
-    it('round should be rounded up', function() {
-      expect(ctx.data.valueRounded).to.be(10);
-    });
-
-    it('Should replace value with text', function() {
-      expect(ctx.data.valueFormatted).to.be('OK');
-    });
-  });
-
-  singleStatScenario('When range to text mapping is specified for first range', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[41, 50]] }];
-      ctx.ctrl.panel.mappingType = 2;
-      ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
-    });
-
-    it('Should replace value with text OK', function() {
-      expect(ctx.data.valueFormatted).to.be('OK');
-    });
-  });
-
-  singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [{ target: 'test.cpu1', datapoints: [[65, 75]] }];
-      ctx.ctrl.panel.mappingType = 2;
-      ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
-    });
-
-    it('Should replace value with text NOT OK', function() {
-      expect(ctx.data.valueFormatted).to.be('NOT OK');
-    });
-  });
-
-  describe('When table data', function() {
-    const tableData = [
-      {
-        columns: [{ text: 'Time', type: 'time' }, { text: 'test1' }, { text: 'mean' }, { text: 'test2' }],
-        rows: [[1492759673649, 'ignore1', 15, 'ignore2']],
-        type: 'table',
-      },
-    ];
-
-    singleStatScenario('with default values', function(ctx) {
-      ctx.setup(function() {
-        ctx.data = tableData;
-        ctx.ctrl.panel.tableColumn = 'mean';
-      });
-
-      it('Should use first rows value as default main value', function() {
-        expect(ctx.data.value).to.be(15);
-        expect(ctx.data.valueRounded).to.be(15);
-      });
-
-      it('should set formatted value', function() {
-        expect(ctx.data.valueFormatted).to.be('15');
-      });
-    });
-
-    singleStatScenario('When table data has multiple columns', function(ctx) {
-      ctx.setup(function() {
-        ctx.data = tableData;
-        ctx.ctrl.panel.tableColumn = '';
-      });
-
-      it('Should set column to first column that is not time', function() {
-        expect(ctx.ctrl.panel.tableColumn).to.be('test1');
-      });
-    });
-
-    singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function(
-      ctx
-    ) {
-      ctx.setup(function() {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2'];
-        ctx.ctrl.panel.tableColumn = 'mean';
-      });
-
-      it('Should be rounded', function() {
-        expect(ctx.data.value).to.be(99.99999);
-        expect(ctx.data.valueRounded).to.be(100);
-      });
-
-      it('should set formatted falue', function() {
-        expect(ctx.data.valueFormatted).to.be('100');
-      });
-    });
-
-    singleStatScenario('When value to text mapping is specified', function(ctx) {
-      ctx.setup(function() {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2'];
-        ctx.ctrl.panel.tableColumn = 'mean';
-        ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
-      });
-
-      it('value should remain', function() {
-        expect(ctx.data.value).to.be(9.9);
-      });
-
-      it('round should be rounded up', function() {
-        expect(ctx.data.valueRounded).to.be(10);
-      });
-
-      it('Should replace value with text', function() {
-        expect(ctx.data.valueFormatted).to.be('OK');
-      });
-    });
-
-    singleStatScenario('When range to text mapping is specified for first range', function(ctx) {
-      ctx.setup(function() {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2'];
-        ctx.ctrl.panel.tableColumn = 'mean';
-        ctx.ctrl.panel.mappingType = 2;
-        ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
-      });
-
-      it('Should replace value with text OK', function() {
-        expect(ctx.data.valueFormatted).to.be('OK');
-      });
-    });
-
-    singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) {
-      ctx.setup(function() {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
-        ctx.ctrl.panel.tableColumn = 'mean';
-        ctx.ctrl.panel.mappingType = 2;
-        ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
-      });
-
-      it('Should replace value with text NOT OK', function() {
-        expect(ctx.data.valueFormatted).to.be('NOT OK');
-      });
-    });
-
-    singleStatScenario('When value is string', function(ctx) {
-      ctx.setup(function() {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
-        ctx.ctrl.panel.tableColumn = 'test1';
-      });
-
-      it('Should replace value with text NOT OK', function() {
-        expect(ctx.data.valueFormatted).to.be('ignore1');
-      });
-    });
-
-    singleStatScenario('When value is zero', function(ctx) {
-      ctx.setup(function() {
-        ctx.data = tableData;
-        ctx.data[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2'];
-        ctx.ctrl.panel.tableColumn = 'mean';
-      });
-
-      it('Should return zero', function() {
-        expect(ctx.data.value).to.be(0);
-      });
-    });
-  });
-});

From bff7a293562125dc8423919f23a871d7141fa189 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 11:34:14 +0200
Subject: [PATCH 115/380] Cleanup

---
 .../panel/singlestat/specs/singlestat.jest.ts | 26 -------------------
 1 file changed, 26 deletions(-)

diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
index 552ac2412d6..7e8915ca537 100644
--- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
+++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
@@ -1,6 +1,3 @@
-// import { describe, beforeEach, afterEach, it, sinon, expect, angularMocks } from 'test/lib/common';
-
-// import helpers from 'test/specs/helpers';
 import { SingleStatCtrl } from '../module';
 import moment from 'moment';
 
@@ -30,17 +27,6 @@ describe('SingleStatCtrl', function() {
   function singleStatScenario(desc, func) {
     describe(desc, function() {
       ctx.setup = function(setupFunc) {
-        // beforeEach(angularMocks.module('grafana.services'));
-        // beforeEach(angularMocks.module('grafana.controllers'));
-        // beforeEach(
-        //   angularMocks.module(function($compileProvider) {
-        //     $compileProvider.preAssignBindingsEnabled(true);
-        //   })
-        // );
-
-        // beforeEach(ctx.providePhase());
-        // beforeEach(ctx.createPanelController(SingleStatCtrl));
-
         beforeEach(function() {
           ctx.ctrl = new SingleStatCtrl($scope, $injector, {});
           setupFunc();
@@ -107,7 +93,6 @@ describe('SingleStatCtrl', function() {
       ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsIso';
-      //   ctx.setIsUtc(true);
       ctx.ctrl.dashboard.isTimezoneUtc = () => true;
     });
 
@@ -139,7 +124,6 @@ describe('SingleStatCtrl', function() {
       ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeAsUS';
-      //   ctx.setIsUtc(true);
       ctx.ctrl.dashboard.isTimezoneUtc = () => true;
     });
 
@@ -149,11 +133,6 @@ describe('SingleStatCtrl', function() {
   });
 
   singleStatScenario('showing last time from now instead of value', function(ctx) {
-    beforeEach(() => {
-      //   clock = sinon.useFakeTimers(epoch);
-      //jest.useFakeTimers();
-    });
-
     ctx.setup(function() {
       ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
@@ -168,10 +147,6 @@ describe('SingleStatCtrl', function() {
     it('should set formatted value', function() {
       expect(ctx.data.valueFormatted).toBe('2 days ago');
     });
-
-    afterEach(() => {
-      //   jest.clearAllTimers();
-    });
   });
 
   singleStatScenario('showing last time from now instead of value (in UTC)', function(ctx) {
@@ -179,7 +154,6 @@ describe('SingleStatCtrl', function() {
       ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
       ctx.ctrl.panel.valueName = 'last_time';
       ctx.ctrl.panel.format = 'dateTimeFromNow';
-      //   ctx.setIsUtc(true);
     });
 
     it('should set formatted value', function() {

From e43feb7bfa0551125f82dbcf6503564227f091a1 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 27 Jul 2018 13:21:40 +0200
Subject: [PATCH 116/380] use const for rowlimit in sql engine

---
 pkg/tsdb/sql_engine.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index 9321e8912dc..27ed37923a3 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -100,6 +100,8 @@ var NewSqlQueryEndpoint = func(config *SqlQueryEndpointConfiguration, rowTransfo
 	return &queryEndpoint, nil
 }
 
+const rowLimit = 1000000
+
 // Query is the main function for the SqlQueryEndpoint
 func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *TsdbQuery) (*Response, error) {
 	result := &Response{
@@ -164,7 +166,6 @@ func (e *sqlQueryEndpoint) transformToTable(query *Query, rows *core.Rows, resul
 		return err
 	}
 
-	rowLimit := 1000000
 	rowCount := 0
 	timeIndex := -1
 
@@ -225,7 +226,6 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 		return err
 	}
 
-	rowLimit := 1000000
 	rowCount := 0
 	timeIndex := -1
 	metricIndex := -1

From 67c613a45a3ab3b15b587e6999e83a63d52a1582 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 13:29:57 +0200
Subject: [PATCH 117/380] Begin conversion

---
 public/app/core/specs/backend_srv.jest.ts | 39 +++++++++++++++++++++++
 1 file changed, 39 insertions(+)
 create mode 100644 public/app/core/specs/backend_srv.jest.ts

diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.jest.ts
new file mode 100644
index 00000000000..6281f3814ce
--- /dev/null
+++ b/public/app/core/specs/backend_srv.jest.ts
@@ -0,0 +1,39 @@
+import { BackendSrv } from 'app/core/services/backend_srv';
+jest.mock('app/core/store');
+
+describe('backend_srv', function() {
+  let _httpBackend = options => {
+    if (options.method === 'GET' && options.url === 'gateway-error') {
+      return Promise.reject({ status: 502 });
+    } else if (options.method === 'POST') {
+      // return Promise.resolve({});
+    }
+    return Promise.resolve({});
+  };
+
+  let _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {});
+
+  //   beforeEach(angularMocks.module('grafana.core'));
+  //   beforeEach(angularMocks.module('grafana.services'));
+  //   beforeEach(
+  //     angularMocks.inject(function($httpBackend, $http, backendSrv) {
+  //       _httpBackend = $httpBackend;
+  //       _backendSrv = backendSrv;
+  //     })
+  //   );
+
+  describe('when handling errors', function() {
+    it('should return the http status code', function(done) {
+      //   _httpBackend.whenGET('gateway-error').respond(502);
+      _backendSrv
+        .datasourceRequest({
+          url: 'gateway-error',
+        })
+        .catch(function(err) {
+          expect(err.status).toBe(502);
+          done();
+        });
+      //   _httpBackend.flush();
+    });
+  });
+});

From b4ac3f2379e675439f571c308eb36581d4a39984 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 27 Jul 2018 13:33:50 +0200
Subject: [PATCH 118/380] update devenv datasources and dashboards for sql
 datasources

---
 devenv/dev-dashboards/datasource_tests_mssql_fakedata.json    | 1 -
 devenv/dev-dashboards/datasource_tests_mssql_unittest.json    | 1 -
 devenv/dev-dashboards/datasource_tests_mysql_fakedata.json    | 1 -
 devenv/dev-dashboards/datasource_tests_mysql_unittest.json    | 1 -
 devenv/dev-dashboards/datasource_tests_postgres_fakedata.json | 1 -
 devenv/dev-dashboards/datasource_tests_postgres_unittest.json | 1 -
 6 files changed, 6 deletions(-)

diff --git a/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
index 4350b5e44a8..e810a686134 100644
--- a/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json
@@ -16,7 +16,6 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": 203,
   "iteration": 1532618661457,
   "links": [],
   "panels": [
diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
index 5c8eb8243a3..d47cfb0ad6e 100644
--- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
@@ -64,7 +64,6 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": 35,
   "iteration": 1532618879985,
   "links": [],
   "panels": [
diff --git a/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
index cef8fd4783f..ebeb452fc4c 100644
--- a/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json
@@ -16,7 +16,6 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": 4,
   "iteration": 1532620738041,
   "links": [],
   "panels": [
diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
index 2c20969da12..326114ec8ff 100644
--- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
@@ -64,7 +64,6 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": 39,
   "iteration": 1532620354037,
   "links": [],
   "panels": [
diff --git a/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
index 1afa6e25df8..508cae86bc3 100644
--- a/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json
@@ -16,7 +16,6 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": 5,
   "iteration": 1532620601931,
   "links": [],
   "panels": [
diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
index d7d5f238e85..85151089b7f 100644
--- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
@@ -64,7 +64,6 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": 38,
   "iteration": 1532619575136,
   "links": [],
   "panels": [

From 55111c801fbdc74687d74136dc73daf2aa29131c Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 13:41:07 +0200
Subject: [PATCH 119/380] Update test for local time

---
 .../plugins/panel/singlestat/specs/singlestat.jest.ts    | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
index 7e8915ca537..dd02b5c169c 100644
--- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
+++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
@@ -23,6 +23,9 @@ describe('SingleStatCtrl', function() {
   SingleStatCtrl.prototype.dashboard = {
     isTimezoneUtc: jest.fn(() => true),
   };
+  SingleStatCtrl.prototype.events = {
+    on: () => {},
+  };
 
   function singleStatScenario(desc, func) {
     describe(desc, function() {
@@ -84,7 +87,7 @@ describe('SingleStatCtrl', function() {
     });
 
     it('should set formatted value', function() {
-      expect(ctx.data.valueFormatted).toBe('2017-09-17 09:56:37');
+      expect(moment(ctx.data.valueFormatted).isSame('2017-09-17 09:56:37')).toBe(true);
     });
   });
 
@@ -235,7 +238,9 @@ describe('SingleStatCtrl', function() {
     singleStatScenario('with default values', function(ctx) {
       ctx.setup(function() {
         ctx.data = tableData;
-        ctx.ctrl.panel = {};
+        ctx.ctrl.panel = {
+          emit: () => {},
+        };
         ctx.ctrl.panel.tableColumn = 'mean';
         ctx.ctrl.panel.format = 'none';
       });

From 1bb5a57036d435299bc287bb4e93eab92b77f7bd Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Fri, 27 Jul 2018 13:45:16 +0200
Subject: [PATCH 120/380] frontend part with mock-team-list

---
 public/app/features/org/partials/profile.html | 99 +++++++++++--------
 public/app/features/org/profile_ctrl.ts       | 15 +++
 2 files changed, 73 insertions(+), 41 deletions(-)

diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html
index 66e41fbb4b4..96540911290 100644
--- a/public/app/features/org/partials/profile.html
+++ b/public/app/features/org/partials/profile.html
@@ -3,53 +3,70 @@
 <div class="page-container page-body">
   <h3 class="page-sub-heading">User Profile</h3>
 
-	<form name="ctrl.userForm" class="gf-form-group">
+  <form name="ctrl.userForm" class="gf-form-group">
 
-		<div class="gf-form max-width-30">
-			<span class="gf-form-label width-8">Name</span>
-			<input class="gf-form-input max-width-22" type="text" required ng-model="ctrl.user.name" >
-		</div>
-		<div class="gf-form max-width-30">
-			<span class="gf-form-label width-8">Email</span>
-			<input class="gf-form-input max-width-22" type="email" ng-readonly="ctrl.readonlyLoginFields" required ng-model="ctrl.user.email">
+    <div class="gf-form max-width-30">
+      <span class="gf-form-label width-8">Name</span>
+      <input class="gf-form-input max-width-22" type="text" required ng-model="ctrl.user.name">
+    </div>
+    <div class="gf-form max-width-30">
+      <span class="gf-form-label width-8">Email</span>
+      <input class="gf-form-input max-width-22" type="email" ng-readonly="ctrl.readonlyLoginFields" required ng-model="ctrl.user.email">
       <i ng-if="ctrl.readonlyLoginFields" class="fa fa-lock gf-form-icon--right-absolute" bs-tooltip="'Login Details Locked - managed in another system.'"></i>
     </div>
-		<div class="gf-form max-width-30">
-			<span class="gf-form-label width-8">Username</span>
+    <div class="gf-form max-width-30">
+      <span class="gf-form-label width-8">Username</span>
       <input class="gf-form-input max-width-22" type="text" ng-readonly="ctrl.readonlyLoginFields" required ng-model="ctrl.user.login">
       <i ng-if="ctrl.readonlyLoginFields" class="fa fa-lock gf-form-icon--right-absolute" bs-tooltip="'Login Details Locked - managed in another system.'"></i>
     </div>
-		<div class="gf-form-button-row">
-			<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
-		</div>
-	</form>
+    <div class="gf-form-button-row">
+      <button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
+    </div>
+  </form>
 
-	<prefs-control mode="user"></prefs-control>
+  <prefs-control mode="user"></prefs-control>
 
-	<h3 class="page-heading" ng-show="ctrl.showOrgsList">Organizations</h3>
+  <h3 class="page-heading">Teams</h3>
+  <div class="gf-form-group" ng-show="ctrl.showTeamsList">
+    <table class="filter-table form-inline">
+      <thead>
+        <tr>
+          <th>Name</th>
+          <th>Email</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="team in ctrl.user.teams">
+          <td>{{team.name}}</td>
+          <td>{{team.email}}</td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <h3 class="page-heading" ng-show="ctrl.showOrgsList">Organizations</h3>
   <div class="gf-form-group" ng-show="ctrl.showOrgsList">
-		<table class="filter-table form-inline">
-			<thead>
-				<tr>
-					<th>Name</th>
-					<th>Role</th>
-					<th></th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr ng-repeat="org in ctrl.orgs">
-					<td>{{org.name}}</td>
-					<td>{{org.role}}</td>
-					<td class="text-right">
-						<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
-							Current
-						</span>
-						<a ng-click="ctrl.setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
-							Select
-						</a>
-					</td>
-				</tr>
-			</tbody>
-		</table>
-	</div>
-
+    <table class="filter-table form-inline">
+      <thead>
+        <tr>
+          <th>Name</th>
+          <th>Role</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="org in ctrl.orgs">
+          <td>{{org.name}}</td>
+          <td>{{org.role}}</td>
+          <td class="text-right">
+            <span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
+              Current
+            </span>
+            <a ng-click="ctrl.setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
+              Select
+            </a>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts
index 5c62a7a5fdb..1ac950699be 100644
--- a/public/app/features/org/profile_ctrl.ts
+++ b/public/app/features/org/profile_ctrl.ts
@@ -4,8 +4,10 @@ import { coreModule } from 'app/core/core';
 export class ProfileCtrl {
   user: any;
   old_theme: any;
+  teams: any = [];
   orgs: any = [];
   userForm: any;
+  showTeamsList = false;
   showOrgsList = false;
   readonlyLoginFields = config.disableLoginForm;
   navModel: any;
@@ -13,6 +15,7 @@ export class ProfileCtrl {
   /** @ngInject **/
   constructor(private backendSrv, private contextSrv, private $location, navModelSrv) {
     this.getUser();
+    this.getUserTeams();
     this.getUserOrgs();
     this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0);
   }
@@ -24,6 +27,18 @@ export class ProfileCtrl {
     });
   }
 
+  getUserTeams() {
+    console.log(this.backendSrv.get('/api/teams'));
+    this.backendSrv.get('/api/user').then(teams => {
+      this.user.teams = [
+        { name: 'Backend', email: 'backend@grafana.com', members: 2 },
+        { name: 'Frontend', email: 'frontend@grafana.com', members: 2 },
+        { name: 'Ops', email: 'ops@grafana.com', members: 2 },
+      ];
+      this.showTeamsList = this.user.teams.length > 1;
+    });
+  }
+
   getUserOrgs() {
     this.backendSrv.get('/api/user/orgs').then(orgs => {
       this.orgs = orgs;

From 971e52ecc98126788066f0452aeaa7bf93f7baf2 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Fri, 27 Jul 2018 13:48:14 +0200
Subject: [PATCH 121/380] removed unused class from the deletebutton pr

---
 public/app/containers/Teams/TeamList.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx
index b86763d8799..31406250cb3 100644
--- a/public/app/containers/Teams/TeamList.tsx
+++ b/public/app/containers/Teams/TeamList.tsx
@@ -88,7 +88,7 @@ export class TeamList extends React.Component<Props, any> {
             </a>
           </div>
 
-          <div className="admin-list-table tr-overflow">
+          <div className="admin-list-table">
             <table className="filter-table filter-table--hover form-inline">
               <thead>
                 <tr>

From 4e6168f3a331e5701e279305774413eca87499d4 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 14:22:48 +0200
Subject: [PATCH 122/380] Add async/await

---
 public/app/core/specs/backend_srv.jest.ts | 22 +++++++++-------------
 1 file changed, 9 insertions(+), 13 deletions(-)

diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.jest.ts
index 6281f3814ce..2d62716622a 100644
--- a/public/app/core/specs/backend_srv.jest.ts
+++ b/public/app/core/specs/backend_srv.jest.ts
@@ -3,10 +3,9 @@ jest.mock('app/core/store');
 
 describe('backend_srv', function() {
   let _httpBackend = options => {
-    if (options.method === 'GET' && options.url === 'gateway-error') {
+    console.log(options);
+    if (options.url === 'gateway-error') {
       return Promise.reject({ status: 502 });
-    } else if (options.method === 'POST') {
-      // return Promise.resolve({});
     }
     return Promise.resolve({});
   };
@@ -22,17 +21,14 @@ describe('backend_srv', function() {
   //     })
   //   );
 
-  describe('when handling errors', function() {
-    it('should return the http status code', function(done) {
+  describe('when handling errors', () => {
+    it('should return the http status code', async () => {
       //   _httpBackend.whenGET('gateway-error').respond(502);
-      _backendSrv
-        .datasourceRequest({
-          url: 'gateway-error',
-        })
-        .catch(function(err) {
-          expect(err.status).toBe(502);
-          done();
-        });
+      let res = await _backendSrv.datasourceRequest({
+        url: 'gateway-error',
+      });
+      console.log(res);
+      expect(res.status).toBe(502);
       //   _httpBackend.flush();
     });
   });

From 2db4a54f75c7c1bd8a3a70ea0d4be50f88ab0552 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 14:40:56 +0200
Subject: [PATCH 123/380] Fix test

---
 public/app/plugins/panel/singlestat/specs/singlestat.jest.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
index dd02b5c169c..0480d0be5c3 100644
--- a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
+++ b/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
@@ -87,7 +87,7 @@ describe('SingleStatCtrl', function() {
     });
 
     it('should set formatted value', function() {
-      expect(moment(ctx.data.valueFormatted).isSame('2017-09-17 09:56:37')).toBe(true);
+      expect(moment(ctx.data.valueFormatted).valueOf()).toBe(1505634997000);
     });
   });
 

From 766c23a1eb86d6ba47b2d61d9b72153089b73264 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 15:16:19 +0200
Subject: [PATCH 124/380] Fix emit errors

---
 public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
index 3ebcf6cdf31..a0c7dd0ab9c 100644
--- a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
+++ b/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
@@ -34,6 +34,9 @@ describe('GraphCtrl', () => {
 
   beforeEach(() => {
     ctx.ctrl = new GraphCtrl(scope, injector, {});
+    ctx.ctrl.events = {
+      emit: () => {},
+    };
     ctx.ctrl.annotationsPromise = Promise.resolve({});
     ctx.ctrl.updateTimeRange();
   });

From b28a362635876bc321063127f0e3ddf3d599cb79 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Sat, 21 Jul 2018 11:04:05 +0200
Subject: [PATCH 125/380] Use metric column as prefix

If multiple value columns are returned and a metric column is
returned aswell the metric column will be used as prefix for
the series name
---
 docs/sources/features/datasources/postgres.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index f9af60a2efc..f3e52ed6652 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -101,7 +101,7 @@ The resulting table panel:
 
 If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
 Any column except `time` and `metric` is treated as a value column.
-You may return a column named `metric` that is used as metric name for the value column.
+You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name.
 
 **Example with `metric` column:**
 

From f9d6c88a556142791bc6ba0af96ca46dd0dac037 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 24 Jul 2018 18:31:47 +0200
Subject: [PATCH 126/380] add testcase for metric column as prefix

---
 pkg/tsdb/postgres/postgres_test.go | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index 089829bf590..c7787929a9d 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -568,6 +568,31 @@ func TestPostgres(t *testing.T) {
 				So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
 			})
 
+			Convey("When doing a metric query with metric column and multiple value columns", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT $__timeEpoch(time), measurement as metric, "valueOne", "valueTwo" FROM metric_values ORDER BY 1`,
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				So(len(queryResult.Series), ShouldEqual, 4)
+				So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+				So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+				So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+				So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
+			})
+
 			Convey("When doing a metric query grouping by time should return correct series", func() {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{

From 7905c29875a29d230af476e41cb070b13bc9de73 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 24 Jul 2018 19:25:48 +0200
Subject: [PATCH 127/380] adjust metric prefix code to sql engine refactor

---
 pkg/tsdb/sql_engine.go                            | 15 ++++++++++++++-
 .../postgres/partials/query.editor.html           |  5 ++++-
 2 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index 27ed37923a3..027f37fc243 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -229,6 +229,8 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 	rowCount := 0
 	timeIndex := -1
 	metricIndex := -1
+	metricPrefix := false
+	var metricPrefixValue string
 
 	// check columns of resultset: a column named time is mandatory
 	// the first text column is treated as metric name unless a column named metric is present
@@ -256,6 +258,11 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 		}
 	}
 
+	// use metric column as prefix with multiple value columns
+	if metricIndex != -1 && len(columnNames) > 3 {
+		metricPrefix = true
+	}
+
 	if timeIndex == -1 {
 		return fmt.Errorf("Found no column named %s", strings.Join(e.timeColumnNames, " or "))
 	}
@@ -301,7 +308,11 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 
 		if metricIndex >= 0 {
 			if columnValue, ok := values[metricIndex].(string); ok {
-				metric = columnValue
+				if metricPrefix {
+					metricPrefixValue = columnValue
+				} else {
+					metric = columnValue
+				}
 			} else {
 				return fmt.Errorf("Column metric must be of type %s. metric column name: %s type: %s but datatype is %T", strings.Join(e.metricColumnTypes, ", "), columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
 			}
@@ -318,6 +329,8 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 
 			if metricIndex == -1 {
 				metric = col
+			} else if metricPrefix {
+				metric = metricPrefixValue + " " + col
 			}
 
 			series, exist := pointsBySeries[metric]
diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html
index 26392c17356..b7c12471f52 100644
--- a/public/app/plugins/datasource/postgres/partials/query.editor.html
+++ b/public/app/plugins/datasource/postgres/partials/query.editor.html
@@ -40,7 +40,10 @@
 		<pre class="gf-form-pre alert alert-info">Time series:
 - return column named <i>time</i> (UTC in seconds or timestamp)
 - return column(s) with numeric datatype as values
-- (Optional: return column named <i>metric</i> to represent the series name. If no column named metric is found the column name of the value column is used as series name)
+Optional: 
+  - return column named <i>metric</i> to represent the series name. 
+  - If multiple value columns are returned the metric column is used as prefix. 
+  - If no column named metric is found the column name of the value column is used as series name
 
 Table:
 - return any set of columns

From 2f6b302375bbe7c562e6df09760f1f4b495b2715 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 15:51:56 +0200
Subject: [PATCH 128/380] Test passing. Remove Karma

---
 public/app/core/specs/backend_srv.jest.ts  | 23 +++++-----------
 public/app/core/specs/backend_srv_specs.ts | 31 ----------------------
 2 files changed, 7 insertions(+), 47 deletions(-)
 delete mode 100644 public/app/core/specs/backend_srv_specs.ts

diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.jest.ts
index 2d62716622a..c65464aa875 100644
--- a/public/app/core/specs/backend_srv.jest.ts
+++ b/public/app/core/specs/backend_srv.jest.ts
@@ -12,24 +12,15 @@ describe('backend_srv', function() {
 
   let _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {});
 
-  //   beforeEach(angularMocks.module('grafana.core'));
-  //   beforeEach(angularMocks.module('grafana.services'));
-  //   beforeEach(
-  //     angularMocks.inject(function($httpBackend, $http, backendSrv) {
-  //       _httpBackend = $httpBackend;
-  //       _backendSrv = backendSrv;
-  //     })
-  //   );
-
   describe('when handling errors', () => {
     it('should return the http status code', async () => {
-      //   _httpBackend.whenGET('gateway-error').respond(502);
-      let res = await _backendSrv.datasourceRequest({
-        url: 'gateway-error',
-      });
-      console.log(res);
-      expect(res.status).toBe(502);
-      //   _httpBackend.flush();
+      try {
+        await _backendSrv.datasourceRequest({
+          url: 'gateway-error',
+        });
+      } catch (err) {
+        expect(err.status).toBe(502);
+      }
     });
   });
 });
diff --git a/public/app/core/specs/backend_srv_specs.ts b/public/app/core/specs/backend_srv_specs.ts
deleted file mode 100644
index 74b058b98c8..00000000000
--- a/public/app/core/specs/backend_srv_specs.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
-import 'app/core/services/backend_srv';
-
-describe('backend_srv', function() {
-  var _backendSrv;
-  var _httpBackend;
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.inject(function($httpBackend, $http, backendSrv) {
-      _httpBackend = $httpBackend;
-      _backendSrv = backendSrv;
-    })
-  );
-
-  describe('when handling errors', function() {
-    it('should return the http status code', function(done) {
-      _httpBackend.whenGET('gateway-error').respond(502);
-      _backendSrv
-        .datasourceRequest({
-          url: 'gateway-error',
-        })
-        .catch(function(err) {
-          expect(err.status).to.be(502);
-          done();
-        });
-      _httpBackend.flush();
-    });
-  });
-});

From c11d0f5cc6289b708d1e0d7c072de7eb6b1b8422 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 15:52:22 +0200
Subject: [PATCH 129/380] Remove lo

---
 public/app/core/specs/backend_srv.jest.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.jest.ts
index c65464aa875..b19bd117766 100644
--- a/public/app/core/specs/backend_srv.jest.ts
+++ b/public/app/core/specs/backend_srv.jest.ts
@@ -3,7 +3,6 @@ jest.mock('app/core/store');
 
 describe('backend_srv', function() {
   let _httpBackend = options => {
-    console.log(options);
     if (options.url === 'gateway-error') {
       return Promise.reject({ status: 502 });
     }

From 895b4b40eee4af0ee79b0935856ff1c532ebeb94 Mon Sep 17 00:00:00 2001
From: Worty <6840978+Worty@users.noreply.github.com>
Date: Fri, 27 Jul 2018 16:26:04 +0200
Subject: [PATCH 130/380] correct volume unit

---
 public/app/core/specs/kbn.jest.ts |  2 +-
 public/app/core/utils/kbn.ts      | 36 +++++++++++++++----------------
 2 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.jest.ts
index 68945068043..9c62990615c 100644
--- a/public/app/core/specs/kbn.jest.ts
+++ b/public/app/core/specs/kbn.jest.ts
@@ -402,7 +402,7 @@ describe('duration', function() {
 describe('volume', function() {
   it('1000m3', function() {
     var str = kbn.valueFormats['m3'](1000, 1, null);
-    expect(str).toBe('1000.0 m3');
+    expect(str).toBe('1000.0 m³');
   });
 });
 
diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts
index 4fc4829811f..74ef2a9e874 100644
--- a/public/app/core/utils/kbn.ts
+++ b/public/app/core/utils/kbn.ts
@@ -572,9 +572,9 @@ kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g');
 // Volume
 kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L');
 kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1);
-kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m3');
-kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm3');
-kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm3');
+kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m³');
+kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm³');
+kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm³');
 kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal');
 
 // Flow
@@ -605,14 +605,14 @@ kbn.valueFormats.radsvh = kbn.formatBuilders.decimalSIPrefix('Sv/h');
 // Concentration
 kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm');
 kbn.valueFormats.conppb = kbn.formatBuilders.fixedUnit('ppb');
-kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m3');
-kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm3');
-kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m3');
-kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm3');
-kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m3');
-kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm3');
-kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m3');
-kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm3');
+kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m³');
+kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm³');
+kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m³');
+kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm³');
+kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m³');
+kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³');
+kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³');
+kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm³');
 
 // Time
 kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
@@ -1119,13 +1119,13 @@ kbn.getUnitFormats = function() {
         { text: 'parts-per-million (ppm)', value: 'ppm' },
         { text: 'parts-per-billion (ppb)', value: 'conppb' },
         { text: 'nanogram per cubic metre (ng/m3)', value: 'conngm3' },
-        { text: 'nanogram per normal cubic metre (ng/Nm3)', value: 'conngNm3' },
-        { text: 'microgram per cubic metre (μg/m3)', value: 'conμgm3' },
-        { text: 'microgram per normal cubic metre (μg/Nm3)', value: 'conμgNm3' },
-        { text: 'milligram per cubic metre (mg/m3)', value: 'conmgm3' },
-        { text: 'milligram per normal cubic metre (mg/Nm3)', value: 'conmgNm3' },
-        { text: 'gram per cubic metre (g/m3)', value: 'congm3' },
-        { text: 'gram per normal cubic metre (g/Nm3)', value: 'congNm3' },
+        { text: 'nanogram per normal cubic metre (ng/Nm³)', value: 'conngNm3' },
+        { text: 'microgram per cubic metre (μg/m³)', value: 'conμgm3' },
+        { text: 'microgram per normal cubic metre (μg/Nm³)', value: 'conμgNm3' },
+        { text: 'milligram per cubic metre (mg/m³)', value: 'conmgm3' },
+        { text: 'milligram per normal cubic metre (mg/Nm³)', value: 'conmgNm3' },
+        { text: 'gram per cubic metre (g/m³)', value: 'congm3' },
+        { text: 'gram per normal cubic metre (g/Nm³)', value: 'congNm3' },
       ],
     },
   ];

From 26f709e87ea5d551b46f3b15909165aee732e298 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 16:45:03 +0200
Subject: [PATCH 131/380] Karm to Jest

---
 ...map_ctrl_specs.ts => heatmap_ctrl.jest.ts} | 44 ++++++++++---------
 1 file changed, 24 insertions(+), 20 deletions(-)
 rename public/app/plugins/panel/heatmap/specs/{heatmap_ctrl_specs.ts => heatmap_ctrl.jest.ts} (61%)

diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
similarity index 61%
rename from public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts
rename to public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
index 98055ccf52d..70449763856 100644
--- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl_specs.ts
+++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
@@ -1,26 +1,30 @@
-import { describe, beforeEach, it, expect, angularMocks } from '../../../../../test/lib/common';
-
 import moment from 'moment';
 import { HeatmapCtrl } from '../heatmap_ctrl';
-import helpers from '../../../../../test/specs/helpers';
 
 describe('HeatmapCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
+  let ctx = <any>{};
 
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
+  let $injector = {
+      get: () => {}
+  };
 
-  beforeEach(ctx.providePhase());
-  beforeEach(ctx.createPanelController(HeatmapCtrl));
-  beforeEach(() => {
-    ctx.ctrl.annotationsPromise = Promise.resolve({});
-    ctx.ctrl.updateTimeRange();
-  });
+  let $scope = {
+    $on: () => {},
+    events: {
+        on: () => {}
+    }
+  };
+
+HeatmapCtrl.prototype.panel = {
+    events: {
+        on: () => {},
+        emit: () => {}
+    }
+};
+
+    beforeEach(() => {
+        ctx.ctrl = new HeatmapCtrl($scope, $injector, {});
+    });
 
   describe('when time series are outside range', function() {
     beforeEach(function() {
@@ -36,7 +40,7 @@ describe('HeatmapCtrl', function() {
     });
 
     it('should set datapointsOutside', function() {
-      expect(ctx.ctrl.dataWarning.title).to.be('Data points outside time range');
+      expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range');
     });
   });
 
@@ -61,7 +65,7 @@ describe('HeatmapCtrl', function() {
     });
 
     it('should set datapointsOutside', function() {
-      expect(ctx.ctrl.dataWarning).to.be(null);
+      expect(ctx.ctrl.dataWarning).toBe(null);
     });
   });
 
@@ -72,7 +76,7 @@ describe('HeatmapCtrl', function() {
     });
 
     it('should set datapointsCount warning', function() {
-      expect(ctx.ctrl.dataWarning.title).to.be('No data points');
+      expect(ctx.ctrl.dataWarning.title).toBe('No data points');
     });
   });
 });

From 805dc3542f780c57f477c61cf9cf475515aa3760 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 27 Jul 2018 16:46:41 +0200
Subject: [PATCH 132/380] Remove extra mock

---
 .../panel/heatmap/specs/heatmap_ctrl.jest.ts  | 21 ++++++++-----------
 1 file changed, 9 insertions(+), 12 deletions(-)

diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
index 70449763856..800c2518f9a 100644
--- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
+++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
@@ -5,26 +5,23 @@ describe('HeatmapCtrl', function() {
   let ctx = <any>{};
 
   let $injector = {
-      get: () => {}
+    get: () => {},
   };
 
   let $scope = {
     $on: () => {},
-    events: {
-        on: () => {}
-    }
   };
 
-HeatmapCtrl.prototype.panel = {
+  HeatmapCtrl.prototype.panel = {
     events: {
-        on: () => {},
-        emit: () => {}
-    }
-};
+      on: () => {},
+      emit: () => {},
+    },
+  };
 
-    beforeEach(() => {
-        ctx.ctrl = new HeatmapCtrl($scope, $injector, {});
-    });
+  beforeEach(() => {
+    ctx.ctrl = new HeatmapCtrl($scope, $injector, {});
+  });
 
   describe('when time series are outside range', function() {
     beforeEach(function() {

From bc9b6ddefe9c982b778d699c7c445db081982fbd Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Fri, 27 Jul 2018 17:14:27 +0200
Subject: [PATCH 133/380] document metric column prefix for mysql and mssql

---
 docs/sources/features/datasources/mssql.md | 2 +-
 docs/sources/features/datasources/mysql.md | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
index d4d5cc6d73e..bcb965dda74 100644
--- a/docs/sources/features/datasources/mssql.md
+++ b/docs/sources/features/datasources/mssql.md
@@ -148,7 +148,7 @@ The resulting table panel:
 
 ## Time series queries
 
-If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric.
+If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name.
 
 **Example database table:**
 
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index ce50053c7ea..c6e620eb08b 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -103,7 +103,7 @@ The resulting table panel:
 
 If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
 Any column except `time` and `metric` is treated as a value column.
-You may return a column named `metric` that is used as metric name for the value column.
+You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name.
 
 **Example with `metric` column:**
 

From 036647ae35b9e6799d5af9b984a47a5907c40d6a Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Fri, 27 Jul 2018 17:18:45 +0200
Subject: [PATCH 134/380] document metric column prefix in query editor

---
 .../app/plugins/datasource/mssql/partials/query.editor.html | 6 ++++--
 .../app/plugins/datasource/mysql/partials/query.editor.html | 5 ++++-
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html
index ddc24475d60..397a35164c0 100644
--- a/public/app/plugins/datasource/mssql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mssql/partials/query.editor.html
@@ -39,9 +39,11 @@
 	<div class="gf-form"  ng-show="ctrl.showHelp">
 		<pre class="gf-form-pre alert alert-info">Time series:
 - return column named time (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
-- optional: return column named metric to represent the series names.
 - any other columns returned will be the time point values.
-- if multiple value columns are present and a metric column is provided. the series name will be the combination of "MetricName - ValueColumnName".
+Optional:
+  - return column named <i>metric</i> to represent the series name.
+  - If multiple value columns are returned the metric column is used as prefix.
+  - If no column named metric is found the column name of the value column is used as series name
 
 Table:
 - return any set of columns
diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html
index df68982fcfa..d4be22fc3e9 100644
--- a/public/app/plugins/datasource/mysql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mysql/partials/query.editor.html
@@ -40,7 +40,10 @@
 		<pre class="gf-form-pre alert alert-info">Time series:
 - return column named time or time_sec (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
 - return column(s) with numeric datatype as values
-- (Optional: return column named <i>metric</i> to represent the series name. If no column named metric is found the column name of the value column is used as series name)
+Optional:
+  - return column named <i>metric</i> to represent the series name.
+  - If multiple value columns are returned the metric column is used as prefix.
+  - If no column named metric is found the column name of the value column is used as series name
 
 Table:
 - return any set of columns

From e487fabcd56f5a04b8fa5a6cba6a020855f2d062 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Fri, 27 Jul 2018 17:54:51 +0200
Subject: [PATCH 135/380] add metric column prefix test for mysql

---
 pkg/tsdb/mysql/mysql_test.go | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
index 3b4e283b726..9947c23498b 100644
--- a/pkg/tsdb/mysql/mysql_test.go
+++ b/pkg/tsdb/mysql/mysql_test.go
@@ -634,6 +634,31 @@ func TestMySQL(t *testing.T) {
 				So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
 			})
 
+			Convey("When doing a metric query with metric column and multiple value columns", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values ORDER BY 1,2`,
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				So(len(queryResult.Series), ShouldEqual, 4)
+				So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+				So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+				So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+				So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
+			})
+
 			Convey("When doing a metric query grouping by time should return correct series", func() {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{

From 3aa4790979cf457a26754afd67f5235fc3345f62 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Fri, 27 Jul 2018 18:13:19 +0200
Subject: [PATCH 136/380] add tests for metric column prefix to mssql

---
 pkg/tsdb/mssql/mssql_test.go | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go
index 86484cb9d5e..8e3d617ca09 100644
--- a/pkg/tsdb/mssql/mssql_test.go
+++ b/pkg/tsdb/mssql/mssql_test.go
@@ -610,6 +610,31 @@ func TestMSSQL(t *testing.T) {
 				So(queryResult.Series[1].Name, ShouldEqual, "valueTwo")
 			})
 
+			Convey("When doing a metric query with metric column and multiple value columns", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT $__timeEpoch(time), measurement AS metric, valueOne, valueTwo FROM metric_values ORDER BY 1",
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				So(len(queryResult.Series), ShouldEqual, 4)
+				So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+				So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+				So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+				So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
+			})
+
 			Convey("Given a stored procedure that takes @from and @to in epoch time", func() {
 				sql := `
 						IF object_id('sp_test_epoch') IS NOT NULL

From 20b2b344f6b230887f9f0625cc10485ccc29dde1 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Sat, 28 Jul 2018 11:31:30 +0200
Subject: [PATCH 137/380] mssql: add logo

---
 .../datasource/mssql/img/sql_server_logo.svg  | 115 ++++++++++++++++++
 .../app/plugins/datasource/mssql/plugin.json  |   4 +-
 2 files changed, 117 insertions(+), 2 deletions(-)
 create mode 100644 public/app/plugins/datasource/mssql/img/sql_server_logo.svg

diff --git a/public/app/plugins/datasource/mssql/img/sql_server_logo.svg b/public/app/plugins/datasource/mssql/img/sql_server_logo.svg
new file mode 100644
index 00000000000..7fb7859c8ac
--- /dev/null
+++ b/public/app/plugins/datasource/mssql/img/sql_server_logo.svg
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   version="1.1"
+   width="385"
+   height="385"
+   id="svg3614">
+  <defs
+     id="defs3616">
+    <linearGradient
+       x1="-411.5267"
+       y1="-29.889751"
+       x2="-23.846952"
+       y2="-258.96371"
+       id="linearGradient2851"
+       xlink:href="#linearGradient3649-2"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.66977913,0,0,0.66977913,488.25553,811.45513)" />
+    <linearGradient
+       id="linearGradient3649-2">
+      <stop
+         id="stop3651-1"
+         style="stop-color:#909ca9;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3653-6"
+         style="stop-color:#ededee;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="-447.79681"
+       y1="-889.05798"
+       x2="-135.85931"
+       y2="-889.05798"
+       id="linearGradient2848"
+       xlink:href="#linearGradient3672-8"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.66977913,0,0,0.66977913,488.25553,811.45513)" />
+    <linearGradient
+       id="linearGradient3672-8">
+      <stop
+         id="stop3674-5"
+         style="stop-color:#939fab;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3676-7"
+         style="stop-color:#dcdee1;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       cx="-1259.0977"
+       cy="-245.42754"
+       r="414.15625"
+       fx="-1259.0977"
+       fy="-245.42754"
+       id="radialGradient2845"
+       xlink:href="#linearGradient3629-6"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-0.6435342,-0.09731428,0.19435637,-1.2852688,-377.15369,-44.777181)" />
+    <linearGradient
+       id="linearGradient3629-6">
+      <stop
+         id="stop3631-1"
+         style="stop-color:#ee352c;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3633-8"
+         style="stop-color:#a91d22;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+  </defs>
+  <metadata
+     id="metadata3619">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-220.96887,-214.23042)"
+     id="layer1">
+    <g
+       transform="matrix(0.87205879,0,0,0.87205879,-814.02071,35.444919)"
+       id="g3545">
+      <g
+         transform="matrix(0.65282401,0,0,0.65282401,1153.1248,109.92721)"
+         id="layer1-2">
+        <path
+           d="m 469.03913,461.90328 -132.3442,43.15889 -115.05546,50.81949 -32.23313,8.51875 c -8.17177,7.76757 -16.8442,15.65704 -26.07959,23.69344 -10.15197,8.83403 -19.69921,16.83977 -26.95861,22.62598 -8.0837,6.44323 -20.10328,18.36933 -26.2469,26.01673 -9.14014,11.37738 -16.287689,23.50961 -19.360836,32.84011 -5.528893,16.78653 -2.835108,33.78858 7.828043,49.47993 13.621163,20.04417 40.737103,40.40934 72.273323,54.27304 16.08642,7.07173 43.2321,16.18811 63.58722,21.32828 33.92184,8.56609 99.48216,17.87306 135.58835,19.25615 7.32269,0.28037 17.07093,0.25405 17.51894,-0.0419 0.79242,-0.52337 6.41334,-11.16652 12.93511,-24.4888 22.19045,-45.32961 38.24499,-87.82466 46.88454,-124.11845 5.23117,-21.97568 9.30946,-51.27956 11.9514,-86.02475 0.73997,-9.73283 1.00822,-42.19356 0.43951,-53.22651 -0.92537,-17.95348 -2.50678,-32.58669 -5.04424,-46.94733 -0.37226,-2.10481 -0.50923,-3.95573 -0.31399,-4.12333 0.36798,-0.31586 1.61798,-0.71689 17.87473,-5.44196 l -3.24421,-7.5978 z m -30.24475,17.74914 c 1.20273,0 4.40105,30.67062 5.23265,50.12878 0.17669,4.13457 0.14099,6.78152 -0.10462,6.78152 -0.78993,0 -16.94869,-9.48862 -28.40285,-16.68169 -10.00288,-6.28166 -28.98503,-18.89586 -32.00292,-21.26549 -0.95222,-0.74772 -0.85162,-0.78423 7.32578,-3.60006 13.91339,-4.79103 46.90054,-15.36306 47.95196,-15.36306 z m -67.41748,22.12364 c 0.8551,-0.002 3.18352,1.28312 8.68623,4.73032 20.64212,12.93129 48.59495,28.48887 60.57315,33.71919 3.71405,1.62171 4.13809,0.99331 -4.41633,6.80245 -18.2344,12.38245 -40.90361,24.5574 -68.73608,36.92157 -4.85087,2.15488 -8.9601,3.91402 -9.12574,3.91402 -0.1657,0 0.37575,-2.46057 1.19301,-5.48381 6.7424,-24.94123 10.54708,-50.11594 10.65364,-70.30588 0.0528,-9.98517 0.0555,-10.03807 1.00467,-10.27692 0.0445,-0.0112 0.11045,-0.0208 0.16745,-0.0209 z m -13.87696,5.29545 c 0.59745,0.60014 0.16483,22.97565 -0.56509,29.0726 -1.7512,14.62678 -4.61304,28.15457 -9.18857,43.53564 -1.09334,3.67531 -2.11824,6.8435 -2.2814,7.03268 -0.39745,0.46078 -14.07996,-12.87072 -18.6283,-18.14683 -7.77131,-9.01483 -13.91915,-17.9699 -18.39796,-26.79116 -2.27631,-4.48349 -5.90618,-13.33805 -5.56754,-13.58396 1.53714,-1.11625 54.25332,-21.49615 54.62886,-21.11897 z m -65.36622,25.76556 c 0.12304,0.0123 0.21861,0.0457 0.27206,0.10465 0.19732,0.2174 0.89409,1.84262 1.54887,3.621 3.18701,8.65635 10.35364,21.51606 16.57703,29.74238 6.79451,8.98129 15.65307,18.55091 23.06552,24.90741 2.38006,2.04101 4.60058,3.94598 4.91872,4.22798 0.63469,0.56261 0.84808,0.46722 -15.46774,6.65593 -18.90941,7.17244 -39.50324,14.35475 -63.14758,21.99806 -9.01047,2.91269 -16.63651,5.37392 -16.93289,5.48382 -0.90065,0.33395 -0.62443,-0.22068 2.00934,-4.33264 11.74846,-18.3418 29.5459,-54.22153 39.496,-79.59906 1.72589,-4.40154 3.36832,-8.80985 3.66289,-9.79552 0.43275,-1.448 0.85919,-1.94051 2.21864,-2.55353 0.75197,-0.33911 1.40989,-0.49735 1.77914,-0.46048 z m -20.03062,8.26759 c 0.26617,0.23414 -4.80586,10.9457 -9.79552,20.67943 -9.67797,18.87958 -20.34802,37.45869 -34.51459,60.13361 -2.44429,3.91242 -4.69682,7.48919 -5.00238,7.93269 -0.50313,0.73036 -0.70936,0.48967 -2.2605,-2.59539 -3.39136,-6.74515 -6.2327,-15.25552 -7.70246,-23.17017 -1.45456,-7.8331 -1.20714,-21.39417 0.5442,-29.80517 1.30111,-6.24889 1.24371,-6.11077 4.18612,-7.61874 12.72165,-6.51994 54.13081,-25.92074 54.54513,-25.55626 z m 172.53089,7.01175 0,4.24891 c -0.0214,22.31631 -2.40351,52.86892 -5.86057,75.16178 -0.60662,3.91204 -1.13193,7.13954 -1.17211,7.17919 -0.0399,0.0395 -2.85983,-0.78685 -6.25822,-1.84189 -15.01651,-4.6618 -31.29677,-11.6262 -45.98456,-19.67476 -9.7266,-5.33 -23.7947,-13.96405 -23.40034,-14.35839 0.10717,-0.10722 4.25062,-2.30294 9.20947,-4.87683 19.6404,-10.19451 38.40192,-21.18544 54.71251,-32.08661 6.11971,-4.09005 15.27646,-10.71038 17.30964,-12.49557 l 1.44418,-1.25583 z m -248.06945,29.57493 c 0.40776,-0.0196 0.30422,0.75423 -0.33489,4.43729 -0.44433,2.56146 -0.9466,7.40004 -1.13018,10.7374 -0.80454,14.62803 1.56768,25.42028 8.83264,40.18674 2.01986,4.10553 3.59993,7.52788 3.51634,7.59781 -0.72738,0.60887 -66.88032,19.96701 -87.67824,25.66091 -6.16505,1.68785 -11.55335,3.17468 -11.9932,3.30704 -0.72168,0.21724 -0.78598,0.0601 -0.5233,-1.63259 2.27799,-14.67589 13.47301,-33.93791 29.11443,-50.10785 10.39893,-10.75028 18.72529,-17.11825 32.88201,-25.20044 10.16014,-5.80048 25.93974,-14.50561 27.10515,-14.94445 0.0783,-0.0295 0.15104,-0.039 0.20924,-0.0419 z m 156.03764,27.94235 c 0.0628,-0.0752 2.51368,1.2368 5.44196,2.93028 21.36749,12.35737 51.18459,23.94792 76.60598,29.76331 l 2.30237,0.52327 -3.16049,1.75817 c -13.27957,7.36881 -57.0128,25.56519 -101.68091,42.32167 -6.52063,2.44614 -12.88327,4.84452 -14.12812,5.3373 -1.24485,0.49277 -2.2605,0.83065 -2.2605,0.73257 0,-0.0981 1.84243,-3.64405 4.1024,-7.8699 12.53953,-23.44641 25.05657,-51.93851 31.47962,-71.70823 0.65484,-2.01549 1.23473,-3.71321 1.29769,-3.78844 z m -15.90725,5.21172 c 0.0686,0.0686 -0.71258,2.16894 -1.73728,4.64659 -8.71744,21.07977 -20.13898,44.07627 -34.70286,69.9082 -3.70629,6.57395 -6.83047,11.94122 -6.94896,11.93044 -0.11862,-0.0108 -3.13302,-1.79443 -6.69779,-3.97681 -21.21318,-12.9868 -39.98213,-28.97251 -52.3056,-44.51938 l -1.75817,-2.19772 9.14664,-2.51167 c 32.62555,-8.93767 60.29345,-18.49568 87.80389,-30.34937 3.8955,-1.67848 7.13161,-2.99883 7.20013,-2.93028 z m 98.81332,34.49363 c 0.0469,0.01 0.0628,0.0426 0.0628,0.0837 0,2.22372 -5.04658,22.80235 -9.25132,37.696 -3.52706,12.49334 -6.48795,22.26528 -11.9723,39.57976 -2.42219,7.64702 -4.50166,13.88841 -4.6257,13.85606 -0.12398,-0.0323 -0.70488,-0.1439 -1.29763,-0.25117 -29.71937,-5.37841 -56.33827,-12.87864 -81.27354,-22.89807 -6.97609,-2.80315 -17.04984,-7.26544 -17.58171,-7.78618 -0.18017,-0.17646 5.84389,-3.01436 13.37469,-6.30011 45.54156,-19.87009 92.7275,-42.49814 108.86,-52.20091 1.94035,-1.16701 3.37683,-1.84667 3.70469,-1.7791 z m -228.10159,7.80711 c 0.2442,0.23133 -12.49486,18.4671 -30.28661,43.3682 -6.18387,8.65476 -13.44816,18.85582 -16.13752,22.66783 -2.68937,3.81203 -6.75754,9.84571 -9.06292,13.41652 l -4.20702,6.48848 -4.47921,-3.7675 c -5.25569,-4.41283 -14.45517,-13.78206 -18.62817,-18.98406 -8.5875,-10.7047 -14.40634,-21.97023 -16.70268,-32.31684 -1.06013,-4.77661 -1.09134,-7.21066 -0.0837,-7.51408 1.4563,-0.43858 28.17205,-6.72799 53.22654,-12.53743 13.91701,-3.22699 30.00678,-7.00436 35.77036,-8.39317 5.76372,-1.38878 10.53462,-2.48127 10.59095,-2.42795 z m 12.80952,4.89776 3.22325,3.60006 c 14.41063,16.05315 29.06627,28.03245 46.94736,38.36579 3.15707,1.82441 5.56326,3.41056 5.35824,3.53727 -0.74426,0.46 -61.83334,22.18032 -90.0853,32.02381 -15.9331,5.55138 -29.02126,10.07794 -29.09353,10.06762 -0.0723,-0.0106 -0.97479,-0.60081 -1.98837,-1.31863 l -1.84189,-1.31863 2.88835,-4.16518 c 9.37383,-13.57822 21.15136,-28.51041 46.92647,-59.52662 l 17.66542,-21.26549 z m 79.82924,57.05681 c 0.0707,-0.0882 4.53708,1.49528 9.94207,3.51634 13.0284,4.87154 23.25352,8.04885 37.08901,11.51183 16.99337,4.25336 41.55116,8.5067 56.09401,9.73272 2.23257,0.18819 3.3722,0.44297 3.014,0.66978 -0.67513,0.42754 -15.40023,5.34585 -26.226,8.74899 -17.18459,5.402 -69.70184,20.91648 -112.5229,33.23779 -7.94351,2.28564 -14.73534,4.2168 -15.09099,4.29078 -0.95912,0.19948 -4.3117,-0.68047 -4.3117,-1.13026 0,-0.21379 2.40109,-3.25252 5.31637,-6.73965 14.39308,-17.21623 28.69146,-36.44489 40.62632,-54.62886 3.2643,-4.97354 5.99921,-9.12129 6.06981,-9.20946 z m -17.68632,0.50233 c 0.12879,0.12882 -6.96618,11.48947 -19.4445,31.14473 -5.26881,8.29909 -11.24331,17.74618 -13.29096,20.99339 -2.04758,3.24725 -5.04799,8.18192 -6.65593,10.98857 l -2.93028,5.10706 -1.48604,-0.39768 c -3.5858,-0.9708 -28.87947,-9.90623 -35.56112,-12.55836 -8.28115,-3.28696 -16.91132,-7.25644 -23.27483,-10.69553 -7.95208,-4.29764 -18.03755,-10.6749 -17.24681,-10.92578 0.22766,-0.0722 13.91165,-3.81738 30.39123,-8.30944 43.83966,-11.94992 68.08908,-18.815 83.99446,-23.77716 2.97194,-0.92717 5.44784,-1.62666 5.50478,-1.5698 z m 124.51609,29.19819 c 0.0944,-0.0205 0.13817,-0.0209 0.14655,0 0.41466,1.03688 -15.8149,45.98847 -21.74693,60.23826 -1.32857,3.19176 -1.82019,3.96509 -2.51167,3.93495 -1.67907,-0.0733 -25.21156,-3.3635 -39.45414,-5.52568 -24.9328,-3.78505 -66.6954,-11.07791 -77.23391,-13.4793 l -2.44891,-0.5442 14.94451,-3.36982 c 32.03205,-7.21707 47.41561,-11.0878 63.022,-15.8654 19.68246,-6.02544 39.17203,-13.53787 58.85684,-22.68877 3.11213,-1.44671 5.76479,-2.5564 6.42566,-2.70004 z"
+           id="path3639"
+           style="fill:url(#linearGradient2851);fill-opacity:1" />
+        <path
+           d="m 332.64704,153.47532 c -2.21329,-0.2584 -37.73187,12.5384 -60.55222,21.80969 -30.92132,12.56253 -54.92967,24.5993 -69.74075,34.99596 -5.51288,3.86977 -12.44517,10.82962 -13.45838,13.50023 -0.37916,0.99939 -0.55705,2.16745 -0.56512,3.36983 l 13.43744,12.68394 31.8773,10.17227 75.93621,13.56303 86.79919,14.92352 0.87908,-7.4513 c -0.2626,-0.0414 -0.51109,-0.0842 -0.77443,-0.12558 l -11.42811,-1.80003 -2.32329,-4.08147 c -11.79701,-20.81606 -24.83366,-46.57511 -32.40057,-64.00577 -5.86027,-13.49938 -11.48771,-29.05026 -14.60955,-40.31233 -1.8541,-6.68868 -2.04712,-7.12176 -3.0768,-7.24199 z m -1.67445,5.14893 c 0.0883,-0.0159 0.13275,-0.0165 0.14651,0 0.0701,0.0839 0.47344,2.87583 0.87909,6.19546 1.70838,13.98057 4.83676,27.53945 9.73273,42.13329 3.68933,10.99713 3.74223,10.35122 -0.64885,9.12574 -10.20664,-2.84851 -55.93699,-10.69829 -89.01783,-15.27933 -5.3303,-0.73814 -9.76678,-1.39454 -9.83739,-1.46515 -0.42002,-0.42002 23.85716,-13.1396 34.61921,-18.14682 13.80301,-6.42205 51.38994,-22.07103 54.12653,-22.56319 z m -96.3854,44.77055 3.87216,1.31863 c 21.23855,7.22952 74.474,17.39861 103.89948,19.8422 3.31964,0.27568 6.11084,0.57143 6.19546,0.64885 0.0846,0.0774 -2.73544,1.58422 -6.25825,3.3489 -14.19511,7.11074 -29.8186,15.77776 -40.62629,22.54225 -3.17389,1.98653 -6.08469,3.58875 -6.46755,3.5582 -0.38286,-0.0305 -2.47593,-0.36679 -4.66753,-0.73257 l -3.99774,-0.64884 -10.04669,-9.75366 c -17.63706,-17.07029 -31.40719,-30.2763 -36.7332,-35.26806 l -5.16985,-4.8559 z m -3.93496,3.13959 14.06537,17.56077 c 7.74292,9.65889 15.50294,19.24456 17.22588,21.30735 1.72293,2.0628 3.05584,3.81094 2.97214,3.87216 -0.39911,0.29195 -20.38542,-3.5993 -30.97728,-6.02801 -10.87353,-2.49331 -15.37086,-3.68678 -22.10271,-5.83964 l -5.48382,-1.75817 0.0209,-1.36049 c 0.0697,-6.74572 8.57225,-16.69555 23.00272,-26.87488 l 1.27677,-0.87909 z m 119.78582,23.92367 c 0.42188,0.0326 0.8674,0.89008 2.11399,3.621 3.41149,7.47361 14.0307,27.72893 16.59796,31.64706 0.84547,1.29036 2.18584,1.37673 -11.90951,-0.90001 -33.87492,-5.47162 -44.83374,-7.3032 -44.83334,-7.51409 2.4e-4,-0.12853 1.01589,-0.79393 2.2605,-1.48607 10.49029,-5.8337 21.07471,-13.17639 30.49588,-21.11898 2.25261,-1.89907 4.4172,-3.70835 4.81404,-4.0396 0.16319,-0.13622 0.31985,-0.22016 0.46048,-0.20931 z"
+           id="path3667"
+           style="fill:url(#linearGradient2848);fill-opacity:1" />
+        <path
+           d="m 189.49624,222.43029 c 0,0 -2.19802,3.48486 -0.12559,8.66526 1.28236,3.20557 5.13312,7.05619 9.37691,11.13508 0,0 44.4523,43.36164 49.89855,49.64738 24.62938,28.42583 35.32656,56.41067 36.31462,95.04581 0.63401,24.79275 -4.14775,46.58763 -15.76078,71.91753 -20.81466,45.40038 -64.672,95.4667 -132.40696,151.13988 l 9.94207,-3.28613 c 6.39049,-4.78745 15.06889,-9.90061 35.54015,-21.09805 47.14502,-25.7875 100.08704,-49.50759 165.12146,-73.94776 93.56794,-35.16314 247.48095,-76.42006 335.05701,-89.83413 l 9.12574,-1.40238 -1.40235,-2.19768 c -8.00773,-12.44678 -13.47363,-20.12184 -20.05151,-28.25631 -19.20861,-23.75425 -42.46295,-43.13612 -70.95473,-59.08711 -39.15285,-21.91946 -89.93571,-38.95523 -154.13289,-51.71947 -12.13647,-2.41309 -38.71491,-6.96926 -60.34295,-10.33972 -45.8141,-7.13958 -75.36328,-11.96085 -108.00188,-17.64453 -11.70198,-2.03037 -29.17404,-4.96447 -40.77281,-7.47219 -6.04368,-1.30667 -17.52946,-4.02262 -26.45627,-7.0955 -7.36978,-2.90169 -17.74774,-5.70666 -19.96779,-14.16998 z m 25.7656,25.01203 c 0.0623,-0.0563 1.73198,0.4602 3.7675,1.15121 3.75137,1.27345 8.70164,2.76425 14.46301,4.37446 4.03609,1.12805 8.47385,2.31161 13.18628,3.51634 6.01455,1.53768 10.98779,2.85882 11.05135,2.93029 0.66583,0.74868 10.76717,32.98709 14.21191,45.35664 1.31478,4.7212 2.31282,8.65475 2.21865,8.74899 -0.0942,0.0942 -1.21478,-1.64411 -2.46981,-3.85123 -11.68618,-20.55124 -30.19298,-41.45042 -51.59396,-58.27079 -2.66753,-2.09661 -4.83493,-3.87661 -4.83493,-3.95591 z m 49.24966,13.60488 c 0.51479,-0.0355 2.73384,0.33597 5.3792,0.92095 17.04192,3.76871 47.4716,9.64348 67.04067,12.93511 3.26035,0.54842 5.94429,1.13903 5.94429,1.31866 0,0.17957 -1.21806,0.94995 -2.70002,1.69535 -3.28339,1.6516 -16.56913,9.60416 -21.01432,12.57932 -11.0969,7.42711 -21.07399,15.37699 -28.34003,22.60504 -2.91997,2.9047 -5.36345,5.27452 -5.44195,5.27452 -0.0785,0 -0.58901,-1.69274 -1.13025,-3.76751 -3.63262,-13.92431 -11.14339,-34.48572 -17.87474,-48.9567 -1.08323,-2.32862 -1.96747,-4.3823 -1.96747,-4.56287 0,-0.0228 0.0308,-0.0368 0.10462,-0.0415 z m 86.63178,16.47242 c 0.57031,0.19008 1.64819,3.55391 3.5582,11.11411 3.65733,14.47668 5.36011,30.73456 4.79307,45.77525 -0.1578,4.18565 -0.4225,8.07258 -0.58606,8.64431 l -0.31392,1.04653 -5.16989,-1.67445 c -10.65733,-3.40656 -28.06998,-8.53352 -42.99145,-12.66298 -8.47693,-2.34597 -15.42582,-4.44037 -15.42582,-4.64659 0,-0.61968 12.36922,-12.98581 17.68632,-17.68639 10.15647,-8.97872 37.48084,-30.23269 38.44955,-29.90979 z m 6.88613,0.9837 c 0.30843,-0.287 41.39844,6.82807 60.07081,10.40254 13.91808,2.66432 34.05452,6.88145 35.2681,7.38847 0.58585,0.24481 -1.49803,1.38042 -8.1839,4.39543 -26.44563,11.92582 -46.0374,22.56968 -65.51277,35.60298 -5.12991,3.43302 -9.39292,6.23732 -9.48153,6.23732 -0.0887,0 -0.16477,-2.86545 -0.16744,-6.36291 -0.0134,-18.99205 -3.80509,-38.1614 -10.75833,-54.35679 -0.74835,-1.7429 -1.30989,-3.23725 -1.23494,-3.30704 z m 106.24372,21.01432 c 0.31814,0.31822 -1.02959,8.42094 -2.19768,13.20724 -3.6357,14.89676 -13.26987,37.00704 -25.15858,57.78935 -2.09501,3.66215 -3.96878,6.71554 -4.18612,6.78151 -0.21741,0.0663 -2.92459,-1.3575 -6.00712,-3.16048 -11.62542,-6.79973 -24.78873,-13.23236 -39.24484,-19.19339 -4.03099,-1.66219 -7.46415,-3.14227 -7.6397,-3.30704 -0.65926,-0.61854 31.46482,-21.88711 48.49623,-32.10754 13.6223,-8.17465 35.55556,-20.39189 35.93781,-20.00965 z m 7.59784,1.19308 c 0.92509,0 19.14786,4.96815 28.57023,7.78618 23.4703,7.01942 50.54446,16.90985 68.10817,24.88645 l 7.30478,3.30704 -5.14893,1.19307 c -42.97491,9.87268 -79.80901,21.26536 -115.2857,35.66574 -2.9479,1.19656 -5.51316,2.17678 -5.69313,2.17678 -0.18003,0 0.76081,-2.27905 2.0721,-5.0652 10.66409,-22.65816 17.55183,-46.43378 19.33987,-66.62213 0.16276,-1.8376 0.4945,-3.32793 0.73261,-3.32793 z m -181.1334,41.63093 c 0.29102,-0.28747 14.16081,2.96223 21.72596,5.08617 11.37077,3.19236 35.56681,11.28879 35.58202,11.90947 0,0.12076 -2.66659,2.44523 -5.9234,5.16989 -13.08138,10.94419 -25.69962,22.3873 -40.81466,36.98434 -4.48096,4.32737 -8.28865,7.849 -8.47686,7.849 -0.18821,0 -0.27186,-0.63173 -0.16744,-1.40238 2.27631,-16.80322 1.7873,-38.37962 -1.38142,-60.34288 -0.40167,-2.78407 -0.64507,-5.15402 -0.5442,-5.25361 z m 292.06554,0.27213 c 0.20802,0.20803 -6.43495,10.59088 -10.56996,16.51421 -6.03345,8.64276 -14.77874,19.98909 -34.70293,45.04264 -10.46568,13.16002 -22.27043,28.05256 -26.24697,33.09131 -3.97649,5.03875 -7.31355,9.1676 -7.40943,9.1676 -0.0958,0 -1.37838,-1.80445 -2.84656,-3.99777 -11.23161,-16.77898 -24.55927,-31.51787 -40.45882,-44.74962 -2.96712,-2.46921 -6.31454,-5.17713 -7.43039,-6.00705 -1.11579,-0.82992 -2.02555,-1.59776 -2.03024,-1.71631 -0.0107,-0.27354 16.97,-7.55631 29.93073,-12.83049 22.56343,-9.1818 53.40235,-20.17643 76.4804,-27.25164 12.16983,-3.731 25.09935,-7.44774 25.28417,-7.26288 z m 7.66059,2.00934 c 0.36443,-0.0743 2.78649,1.09783 5.69313,2.76283 24.30567,13.9231 48.12312,31.80286 66.85233,50.19158 5.32753,5.23064 18.36133,18.78851 18.16776,18.90029 -0.0496,0.0288 -4.55114,0.38526 -10.00483,0.79537 -42.11909,3.16765 -95.9679,12.12762 -147.81188,24.59348 -3.5211,0.8466 -6.56494,1.54887 -6.76058,1.54887 -0.19569,0 3.65828,-3.86858 8.56061,-8.58155 30.44591,-29.27002 44.33563,-47.75183 60.6778,-80.77121 2.43394,-4.91772 4.49766,-9.1603 4.5838,-9.41877 0.005,-0.0161 0.0176,-0.0161 0.0419,-0.0208 z m -222.61781,22.81435 c 1.39073,0.31211 14.3752,6.35392 24.17481,11.26066 8.96171,4.48718 22.53499,11.71377 23.21207,12.34905 0.085,0.0798 -4.69643,2.57657 -10.63275,5.54658 -18.79353,9.40256 -34.89134,18.28202 -51.69857,28.54933 -4.79281,2.92787 -8.81001,5.33734 -8.9374,5.33734 -0.41928,0 -0.25405,-0.36249 2.42795,-5.27451 8.95032,-16.39245 16.17014,-35.96413 20.32364,-55.08934 0.37072,-1.70706 0.8168,-2.74944 1.13025,-2.67911 z m -13.08165,2.34422 c 0.27106,0.27106 -3.09834,12.64697 -5.23265,19.25615 -4.09356,12.67624 -11.01506,28.61129 -17.66543,40.60536 -1.56186,2.81683 -3.95826,6.94005 -5.31637,9.16761 l -2.46981,4.03957 -5.6094,-5.42099 c -6.51822,-6.3191 -11.8277,-10.2432 -18.60727,-13.73048 -2.66471,-1.37063 -4.83165,-2.63919 -4.835,-2.82566 -0.0127,-0.82436 17.05298,-16.34402 30.22378,-27.48184 9.42775,-7.97252 29.27116,-23.8507 29.51215,-23.60972 z m 79.49441,32.6936 4.87686,3.16048 c 11.18772,7.26376 24.43495,17.0353 34.53549,25.45161 5.66023,4.71652 16.66073,14.56689 18.92123,16.95379 l 1.21398,1.2768 -8.10015,2.2605 c -45.81343,12.711 -81.20818,24.02665 -122.50676,39.20298 -4.5833,1.68429 -8.54082,3.07683 -8.81181,3.07683 -0.56972,0 -1.13595,0.5237 9.1676,-8.9583 26.41535,-24.3091 49.75019,-51.09249 67.18722,-77.15018 l 3.51634,-5.27451 z m -20.88874,5.23265 c 0.23931,0.23931 -13.53845,19.53585 -21.72596,30.43309 -9.79532,13.03718 -27.24126,34.83112 -39.24484,49.01946 -5.01517,5.92788 -9.31167,10.84673 -9.54435,10.92577 -0.25525,0.0867 -0.41392,-1.36548 -0.41861,-3.66289 -0.0234,-12.10619 -3.08614,-25.04478 -8.49783,-36.00063 -2.28575,-4.62744 -2.66873,-5.74744 -2.19774,-6.17449 1.88342,-1.70754 31.02524,-18.32389 49.39621,-28.17259 12.61891,-6.76503 31.99307,-16.60777 32.23312,-16.36772 z m -126.33709,30.95632 c 0.2503,0 2.58796,1.17446 5.19079,2.61636 6.37804,3.53321 12.10807,7.42075 17.16309,11.63741 0.19504,0.16269 -2.42259,2.30096 -5.81871,4.75121 -9.47945,6.83932 -23.91439,17.73368 -32.27498,24.38418 -8.82079,7.01654 -9.11335,7.23167 -8.12107,5.71402 6.60583,-10.10288 9.92713,-15.82286 13.39558,-23.12828 3.08333,-6.49424 6.15621,-14.2034 8.35134,-20.9097 0.91191,-2.78608 1.86367,-5.0652 2.11396,-5.0652 z m 33.6564,26.41441 c 0.45539,-0.0563 1.10346,0.79175 3.80937,4.835 5.70277,8.52113 10.06986,19.88474 11.1979,29.1354 l 0.23021,1.98837 -13.66765,5.29547 c -24.53414,9.52085 -47.11769,18.90539 -62.37318,25.89112 -4.2681,1.95442 -11.80338,5.55455 -16.72358,8.01645 -4.92013,2.46184 -8.93733,4.3967 -8.93733,4.29074 0,-0.10589 3.08982,-2.42902 6.86523,-5.16982 29.9406,-21.73594 55.7553,-45.53279 75.20361,-69.32214 2.08,-2.54429 3.96918,-4.75075 4.20708,-4.89776 0.063,-0.0389 0.12331,-0.0549 0.18834,-0.063 z m -15.5305,3.85123 c 0.39604,0.39604 -11.04714,13.33711 -18.75382,21.20273 -19.2889,19.68682 -38.41143,35.12141 -62.10105,50.14971 -2.96397,1.88027 -5.67236,3.58037 -6.02801,3.78841 -0.65029,0.38037 0.21024,-0.59758 10.61178,-11.93044 6.55473,-7.14159 11.57171,-13.07014 17.26778,-20.47013 3.75116,-4.87324 4.46763,-5.55689 9.96296,-9.52339 14.80266,-10.68458 48.64666,-33.61058 49.04036,-33.21689 z"
+           id="path2851"
+           style="fill:url(#radialGradient2845);fill-opacity:1" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/public/app/plugins/datasource/mssql/plugin.json b/public/app/plugins/datasource/mssql/plugin.json
index 65ef82511cd..ac5ea49ebe9 100644
--- a/public/app/plugins/datasource/mssql/plugin.json
+++ b/public/app/plugins/datasource/mssql/plugin.json
@@ -10,8 +10,8 @@
       "url": "https://grafana.com"
     },
     "logos": {
-      "small": "",
-      "large": ""
+      "small": "img/sql_server_logo.svg",
+      "large": "img/sql_server_logo.svg"
     }
   },
 

From e37e8cb38c649796db57a39868d4c3c79ddab9fd Mon Sep 17 00:00:00 2001
From: Jan Garaj <info@monitoringartist.com>
Date: Mon, 30 Jul 2018 08:02:16 +0100
Subject: [PATCH 138/380] Add missing tls_skip_verify_insecure (#12748)

---
 conf/defaults.ini | 1 +
 1 file changed, 1 insertion(+)

diff --git a/conf/defaults.ini b/conf/defaults.ini
index 5faba3ea7bd..6c27886c649 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -311,6 +311,7 @@ token_url =
 api_url =
 team_ids =
 allowed_organizations =
+tls_skip_verify_insecure = false
 
 #################################### Basic Auth ##########################
 [auth.basic]

From 13a7b638bcc90ff6abcf00a388d8dfbedf01a8b2 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 30 Jul 2018 10:19:51 +0200
Subject: [PATCH 139/380] changelog: add notes about closing #12747

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad1b63234e9..4a2c3c7a0af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
+* **Auth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 
 # 5.2.2 (2018-07-25)
 

From e4983cba2fc17de8523814b7126e5c2d858ac569 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 30 Jul 2018 10:21:22 +0200
Subject: [PATCH 140/380] changelog: update

[skip ci]
---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a2c3c7a0af..b8f5bced972 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,7 @@
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
-* **Auth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
+* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 
 # 5.2.2 (2018-07-25)
 

From 3d4a346c6621c6e685d338dc95aed0221c84c541 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 30 Jul 2018 13:02:08 +0200
Subject: [PATCH 141/380] Begin conversion

---
 .../prometheus/specs/_datasource.jest.ts      | 792 ++++++++++++++++++
 1 file changed, 792 insertions(+)
 create mode 100644 public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts

diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
new file mode 100644
index 00000000000..384abc8f902
--- /dev/null
+++ b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
@@ -0,0 +1,792 @@
+import moment from 'moment';
+import { PrometheusDatasource } from '../datasource';
+import $q from 'q';
+
+const SECOND = 1000;
+const MINUTE = 60 * SECOND;
+const HOUR = 60 * MINUTE;
+
+const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND);
+
+let ctx = <any>{};
+let instanceSettings = {
+  url: 'proxied',
+  directUrl: 'direct',
+  user: 'test',
+  password: 'mupp',
+  jsonData: { httpMethod: 'GET' },
+};
+let backendSrv = <any>{
+  datasourceRequest: jest.fn(),
+};
+
+let templateSrv = {
+  replace: (target, scopedVars, format) => {
+    if (!target) {
+      return target;
+    }
+    let variable, value, fmt;
+
+    return target.replace(scopedVars, (match, var1, var2, fmt2, var3, fmt3) => {
+      variable = this.index[var1 || var2 || var3];
+      fmt = fmt2 || fmt3 || format;
+      if (scopedVars) {
+        value = scopedVars[var1 || var2 || var3];
+        if (value) {
+          return this.formatValue(value.value, fmt, variable);
+        }
+      }
+    });
+  },
+};
+
+let timeSrv = {
+  timeRange: () => {
+    return { to: { diff: () => 2000 }, from: '' };
+  },
+};
+
+describe('PrometheusDatasource', function() {
+  //   beforeEach(angularMocks.module('grafana.core'));
+  //   beforeEach(angularMocks.module('grafana.services'));
+  //   beforeEach(ctx.providePhase(['timeSrv']));
+
+  //   beforeEach(
+  //     angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
+  //       ctx.$q = $q;
+  //       ctx.$httpBackend = $httpBackend;
+  //       ctx.$rootScope = $rootScope;
+  //       ctx.ds = $injector.instantiate(PrometheusDatasource, {
+  //         instanceSettings: instanceSettings,
+  //       });
+  //       $httpBackend.when('GET', /\.html$/).respond('');
+  //     })
+  //   );
+
+  beforeEach(() => {
+    ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+  });
+  describe('When querying prometheus with one target using query editor target spec', function() {
+    var results;
+    var query = {
+      range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
+      interval: '60s',
+    };
+    // Interval alignment with step
+    var urlExpected =
+      'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60';
+    var response = {
+      data: {
+        status: 'success',
+        data: {
+          resultType: 'matrix',
+          result: [
+            {
+              metric: { __name__: 'test', job: 'testjob' },
+              values: [[60, '3846']],
+            },
+          ],
+        },
+      },
+    };
+    beforeEach(async () => {
+      //   ctx.$httpBackend.expect('GET', urlExpected).respond(response);
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+      //   ctx.$httpBackend.flush();
+    });
+    it('should generate the correct query', function() {
+      //   ctx.$httpBackend.verifyNoOutstandingExpectation();
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should return series list', function() {
+      expect(results.data.length).toBe(1);
+      expect(results.data[0].target).toBe('test{job="testjob"}');
+    });
+  });
+  describe('When querying prometheus with one target which return multiple series', function() {
+    var results;
+    var start = 60;
+    var end = 360;
+    var step = 60;
+    // var urlExpected =
+    //   'proxied/api/v1/query_range?query=' +
+    //   encodeURIComponent('test{job="testjob"}') +
+    //   '&start=' +
+    //   start +
+    //   '&end=' +
+    //   end +
+    //   '&step=' +
+    //   step;
+    var query = {
+      range: { from: time({ seconds: start }), to: time({ seconds: end }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
+      interval: '60s',
+    };
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'matrix',
+          result: [
+            {
+              metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
+              values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']],
+            },
+            {
+              metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
+              values: [[start + step * 2, '4846']],
+            },
+          ],
+        },
+      },
+    };
+    beforeEach(async () => {
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+    it('should be same length', function() {
+      expect(results.data.length).toBe(2);
+      expect(results.data[0].datapoints.length).toBe((end - start) / step + 1);
+      expect(results.data[1].datapoints.length).toBe((end - start) / step + 1);
+    });
+    it('should fill null until first datapoint in response', function() {
+      expect(results.data[0].datapoints[0][1]).toBe(start * 1000);
+      expect(results.data[0].datapoints[0][0]).toBe(null);
+      expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000);
+      expect(results.data[0].datapoints[1][0]).toBe(3846);
+    });
+    it('should fill null after last datapoint in response', function() {
+      var length = (end - start) / step + 1;
+      expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000);
+      expect(results.data[0].datapoints[length - 2][0]).toBe(3848);
+      expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000);
+      expect(results.data[0].datapoints[length - 1][0]).toBe(null);
+    });
+    it('should fill null at gap between series', function() {
+      expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000);
+      expect(results.data[0].datapoints[2][0]).toBe(null);
+      expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000);
+      expect(results.data[1].datapoints[1][0]).toBe(null);
+      expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000);
+      expect(results.data[1].datapoints[3][0]).toBe(null);
+    });
+  });
+  describe('When querying prometheus with one target and instant = true', function() {
+    var results;
+    var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
+    var query = {
+      range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
+      interval: '60s',
+    };
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'vector',
+          result: [
+            {
+              metric: { __name__: 'test', job: 'testjob' },
+              value: [123, '3846'],
+            },
+          ],
+        },
+      },
+    };
+    beforeEach(async () => {
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+    it('should generate the correct query', function() {
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should return series list', function() {
+      expect(results.data.length).toBe(1);
+      expect(results.data[0].target).toBe('test{job="testjob"}');
+    });
+  });
+  describe('When performing annotationQuery', function() {
+    var results;
+    // var urlExpected =
+    //   'proxied/api/v1/query_range?query=' +
+    //   encodeURIComponent('ALERTS{alertstate="firing"}') +
+    //   '&start=60&end=180&step=60';
+    var options = {
+      annotation: {
+        expr: 'ALERTS{alertstate="firing"}',
+        tagKeys: 'job',
+        titleFormat: '{{alertname}}',
+        textFormat: '{{instance}}',
+      },
+      range: {
+        from: time({ seconds: 63 }),
+        to: time({ seconds: 123 }),
+      },
+    };
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'matrix',
+          result: [
+            {
+              metric: {
+                __name__: 'ALERTS',
+                alertname: 'InstanceDown',
+                alertstate: 'firing',
+                instance: 'testinstance',
+                job: 'testjob',
+              },
+              values: [[123, '1']],
+            },
+          ],
+        },
+      },
+    };
+    beforeEach(async () => {
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+
+      await ctx.ds.annotationQuery(options).then(function(data) {
+        results = data;
+      });
+    });
+    it('should return annotation list', function() {
+      //   ctx.$rootScope.$apply();
+      expect(results.length).toBe(1);
+      expect(results[0].tags).toContain('testjob');
+      expect(results[0].title).toBe('InstanceDown');
+      expect(results[0].text).toBe('testinstance');
+      expect(results[0].time).toBe(123 * 1000);
+    });
+  });
+
+  describe('When resultFormat is table and instant = true', function() {
+    var results;
+    var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
+    var query = {
+      range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
+      interval: '60s',
+    };
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'vector',
+          result: [
+            {
+              metric: { __name__: 'test', job: 'testjob' },
+              value: [123, '3846'],
+            },
+          ],
+        },
+      },
+    };
+
+    beforeEach(async () => {
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+
+    it('should return result', () => {
+      expect(results).not.toBe(null);
+    });
+  });
+
+  describe('The "step" query parameter', function() {
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'matrix',
+          result: [],
+        },
+      },
+    };
+
+    it('should be min interval when greater than auto interval', async () => {
+      let query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '10s',
+          },
+        ],
+        interval: '5s',
+      };
+      let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+
+    it('step should never go below 1', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [{ expr: 'test' }],
+        interval: '100ms',
+      };
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+
+    it('should be auto interval when greater than min interval', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '5s',
+          },
+        ],
+        interval: '10s',
+      };
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should result in querying fewer than 11000 data points', async () => {
+      var query = {
+        // 6 hour range
+        range: { from: time({ hours: 1 }), to: time({ hours: 7 }) },
+        targets: [{ expr: 'test' }],
+        interval: '1s',
+      };
+      var end = 7 * 60 * 60;
+      var start = 60 * 60;
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should not apply min interval when interval * intervalFactor greater', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '10s',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '5s',
+      };
+      // times get rounded up to interval
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should apply min interval when interval * intervalFactor smaller', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '15s',
+            intervalFactor: 2,
+          },
+        ],
+        interval: '5s',
+      };
+      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should apply intervalFactor to auto interval when greater', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '5s',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '10s',
+      };
+      // times get aligned to interval
+      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should not not be affected by the 11000 data points limit when large enough', async () => {
+      var query = {
+        // 1 week range
+        range: { from: time({}), to: time({ hours: 7 * 24 }) },
+        targets: [
+          {
+            expr: 'test',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '10s',
+      };
+      var end = 7 * 24 * 60 * 60;
+      var start = 0;
+      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should be determined by the 11000 data points limit when too small', async () => {
+      var query = {
+        // 1 week range
+        range: { from: time({}), to: time({ hours: 7 * 24 }) },
+        targets: [
+          {
+            expr: 'test',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '5s',
+      };
+      var end = 7 * 24 * 60 * 60;
+      var start = 0;
+      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+  });
+
+  describe('The __interval and __interval_ms template variables', function() {
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'matrix',
+          result: [],
+        },
+      },
+    };
+
+    it('should be unchanged when auto interval is greater than min interval', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '5s',
+          },
+        ],
+        interval: '10s',
+        scopedVars: {
+          __interval: { text: '10s', value: '10s' },
+          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10';
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(query.scopedVars.__interval.text).toBe('10s');
+      expect(query.scopedVars.__interval.value).toBe('10s');
+      expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
+      expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000);
+    });
+    it('should be min interval when it is greater than auto interval', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '10s',
+          },
+        ],
+        interval: '5s',
+        scopedVars: {
+          __interval: { text: '5s', value: '5s' },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(query.scopedVars.__interval.text).toBe('5s');
+      expect(query.scopedVars.__interval.value).toBe('5s');
+      expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000);
+    });
+    it('should account for intervalFactor', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '5s',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '10s',
+        scopedVars: {
+          __interval: { text: '10s', value: '10s' },
+          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[100s])') + '&start=0&end=500&step=100';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(query.scopedVars.__interval.text).toBe('10s');
+      expect(query.scopedVars.__interval.value).toBe('10s');
+      expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
+      expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000);
+    });
+    it('should be interval * intervalFactor when greater than min interval', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '10s',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '5s',
+        scopedVars: {
+          __interval: { text: '5s', value: '5s' },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[50s])') + '&start=50&end=450&step=50';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(query.scopedVars.__interval.text).toBe('5s');
+      expect(query.scopedVars.__interval.value).toBe('5s');
+      expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000);
+    });
+    it('should be min interval when greater than interval * intervalFactor', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '15s',
+            intervalFactor: 2,
+          },
+        ],
+        interval: '5s',
+        scopedVars: {
+          __interval: { text: '5s', value: '5s' },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[15s])') + '&start=60&end=420&step=15';
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(query.scopedVars.__interval.text).toBe('5s');
+      expect(query.scopedVars.__interval.value).toBe('5s');
+      expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000);
+    });
+    it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => {
+      var query = {
+        // 1 week range
+        range: { from: time({}), to: time({ hours: 7 * 24 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '5s',
+        scopedVars: {
+          __interval: { text: '5s', value: '5s' },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
+        },
+      };
+      var end = 7 * 24 * 60 * 60;
+      var start = 0;
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[60s])') +
+        '&start=' +
+        start +
+        '&end=' +
+        end +
+        '&step=60';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(query.scopedVars.__interval.text).toBe('5s');
+      expect(query.scopedVars.__interval.value).toBe('5s');
+      expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000);
+    });
+  });
+});
+
+describe('PrometheusDatasource for POST', function() {
+  //   var ctx = new helpers.ServiceTestContext();
+  let instanceSettings = {
+    url: 'proxied',
+    directUrl: 'direct',
+    user: 'test',
+    password: 'mupp',
+    jsonData: { httpMethod: 'POST' },
+  };
+
+  //   beforeEach(angularMocks.module('grafana.core'));
+  //   beforeEach(angularMocks.module('grafana.services'));
+  //   beforeEach(ctx.providePhase(['timeSrv']));
+
+  //   beforeEach(
+  //     // angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
+  //     //   ctx.$q = $q;
+  //     //   ctx.$httpBackend = $httpBackend;
+  //     //   ctx.$rootScope = $rootScope;
+  //     //   ctx.ds = $injector.instantiate(PrometheusDatasource, { instanceSettings: instanceSettings });
+  //     //   $httpBackend.when('GET', /\.html$/).respond('');
+  //     // })
+  //   );
+
+  describe('When querying prometheus with one target using query editor target spec', function() {
+    var results;
+    var urlExpected = 'proxied/api/v1/query_range';
+    var dataExpected = {
+      query: 'test{job="testjob"}',
+      start: 1 * 60,
+      end: 3 * 60,
+      step: 60,
+    };
+    var query = {
+      range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
+      interval: '60s',
+    };
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'matrix',
+          result: [
+            {
+              metric: { __name__: 'test', job: 'testjob' },
+              values: [[2 * 60, '3846']],
+            },
+          ],
+        },
+      },
+    };
+    beforeEach(async () => {
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+    it('should generate the correct query', function() {
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('POST');
+      expect(res.url).toBe(urlExpected);
+      expect(res.data).toEqual(dataExpected);
+    });
+    it('should return series list', function() {
+      expect(results.data.length).toBe(1);
+      expect(results.data[0].target).toBe('test{job="testjob"}');
+    });
+  });
+});

From e32cf75c2d3caca0d62e3296701d63c9135e2233 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 30 Jul 2018 13:50:18 +0200
Subject: [PATCH 142/380] fix usage of metric column types so that you don't
 need to specify metric alias

---
 pkg/tsdb/sql_engine.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index 027f37fc243..29428971c64 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -75,6 +75,10 @@ var NewSqlQueryEndpoint = func(config *SqlQueryEndpointConfiguration, rowTransfo
 		queryEndpoint.timeColumnNames = config.TimeColumnNames
 	}
 
+	if len(config.MetricColumnTypes) > 0 {
+		queryEndpoint.metricColumnTypes = config.MetricColumnTypes
+	}
+
 	engineCache.Lock()
 	defer engineCache.Unlock()
 
@@ -249,6 +253,7 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 				columnType := columnTypes[i].DatabaseTypeName()
 
 				for _, mct := range e.metricColumnTypes {
+					e.log.Info(mct)
 					if columnType == mct {
 						metricIndex = i
 						continue

From 38a52c2489853eaff1ce036b864f736c59c9ba49 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 30 Jul 2018 13:50:52 +0200
Subject: [PATCH 143/380] mssql: update tests

---
 pkg/tsdb/mssql/mssql_test.go | 54 ++++++++++--------------------------
 1 file changed, 15 insertions(+), 39 deletions(-)

diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go
index 8e3d617ca09..30d1da3bda1 100644
--- a/pkg/tsdb/mssql/mssql_test.go
+++ b/pkg/tsdb/mssql/mssql_test.go
@@ -615,7 +615,7 @@ func TestMSSQL(t *testing.T) {
 					Queries: []*tsdb.Query{
 						{
 							Model: simplejson.NewFromAny(map[string]interface{}{
-								"rawSql": "SELECT $__timeEpoch(time), measurement AS metric, valueOne, valueTwo FROM metric_values ORDER BY 1",
+								"rawSql": "SELECT $__timeEpoch(time), measurement, valueOne, valueTwo FROM metric_values ORDER BY 1",
 								"format": "time_series",
 							}),
 							RefId: "A",
@@ -660,21 +660,9 @@ func TestMSSQL(t *testing.T) {
 
 							SELECT
 								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
-								measurement + ' - value one' as metric,
-								avg(valueOne) as value
-							FROM
-								metric_values
-							WHERE
-								time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND
-								(@metric = 'ALL' OR measurement = @metric)
-							GROUP BY
-								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
-								measurement
-							UNION ALL
-							SELECT
-								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
-								measurement + ' - value two' as metric,
-								avg(valueTwo) as value
+								measurement as metric,
+								avg(valueOne) as valueOne,
+								avg(valueTwo) as valueTwo
 							FROM
 								metric_values
 							WHERE
@@ -717,10 +705,10 @@ func TestMSSQL(t *testing.T) {
 					So(queryResult.Error, ShouldBeNil)
 
 					So(len(queryResult.Series), ShouldEqual, 4)
-					So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one")
-					So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
-					So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two")
-					So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two")
+					So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+					So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+					So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+					So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
 				})
 			})
 
@@ -749,21 +737,9 @@ func TestMSSQL(t *testing.T) {
 
 							SELECT
 								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
-								measurement + ' - value one' as metric,
-								avg(valueOne) as value
-							FROM
-								metric_values
-							WHERE
-								time BETWEEN @from AND @to AND
-								(@metric = 'ALL' OR measurement = @metric)
-							GROUP BY
-								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
-								measurement
-							UNION ALL
-							SELECT
-								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
-								measurement + ' - value two' as metric,
-								avg(valueTwo) as value
+								measurement as metric,
+								avg(valueOne) as valueOne,
+								avg(valueTwo) as valueTwo
 							FROM
 								metric_values
 							WHERE
@@ -806,10 +782,10 @@ func TestMSSQL(t *testing.T) {
 					So(queryResult.Error, ShouldBeNil)
 
 					So(len(queryResult.Series), ShouldEqual, 4)
-					So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one")
-					So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
-					So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two")
-					So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two")
+					So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne")
+					So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo")
+					So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne")
+					So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo")
 				})
 			})
 		})

From 917b6b11b0fbae37d80a5dd097de031327e98679 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 30 Jul 2018 13:54:57 +0200
Subject: [PATCH 144/380] devenv: update sql dashboards

---
 .../datasource_tests_mssql_unittest.json      | 73 ++++---------------
 .../datasource_tests_mysql_unittest.json      | 73 ++++---------------
 .../datasource_tests_postgres_unittest.json   | 73 ++++---------------
 3 files changed, 42 insertions(+), 177 deletions(-)

diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
index d47cfb0ad6e..0c7cc0fcc65 100644
--- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1532618879985,
+  "iteration": 1532949769359,
   "links": [],
   "panels": [
     {
@@ -871,14 +871,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement + ' - value one' as metric, \n  avg(valueOne) as valueOne\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement as metric, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement + ' - value two' as metric, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values\nWHERE\n  $__timeFilter(time) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1067,14 +1061,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1245,14 +1233,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1423,14 +1405,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1773,14 +1749,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1954,14 +1924,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -2135,14 +2099,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -2316,14 +2274,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -2460,7 +2412,10 @@
   "refresh": false,
   "schemaVersion": 16,
   "style": "dark",
-  "tags": ["gdev", "mssql"],
+  "tags": [
+    "gdev",
+    "mssql"
+  ],
   "templating": {
     "list": [
       {
@@ -2587,5 +2542,5 @@
   "timezone": "",
   "title": "Datasource tests - MSSQL (unit test)",
   "uid": "GlAqcPgmz",
-  "version": 58
+  "version": 3
 }
\ No newline at end of file
diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
index 326114ec8ff..e95eedf254c 100644
--- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1532620354037,
+  "iteration": 1532949531280,
   "links": [],
   "panels": [
     {
@@ -871,14 +871,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  CONCAT(measurement, ' - value one') as metric, \n  avg(valueOne) as valueOne\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement as metric, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  CONCAT(measurement, ' - value two') as metric, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values\nWHERE\n  $__timeFilter(time) AND\n  measurement IN($metric)\nGROUP BY 1,2\nORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1061,14 +1055,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1239,14 +1227,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1417,14 +1399,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1593,14 +1569,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1774,14 +1744,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1955,14 +1919,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -2136,14 +2094,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -2280,7 +2232,10 @@
   "refresh": false,
   "schemaVersion": 16,
   "style": "dark",
-  "tags": ["gdev", "mysql"],
+  "tags": [
+    "gdev",
+    "mysql"
+  ],
   "templating": {
     "list": [
       {
@@ -2405,5 +2360,5 @@
   "timezone": "",
   "title": "Datasource tests - MySQL (unittest)",
   "uid": "Hmf8FDkmz",
-  "version": 12
+  "version": 1
 }
\ No newline at end of file
diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
index 85151089b7f..2243baed0aa 100644
--- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1532619575136,
+  "iteration": 1532951521836,
   "links": [],
   "panels": [
     {
@@ -871,14 +871,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize'), \n  measurement || ' - value one' as metric, \n  avg(\"valueOne\") as \"valueOne\"\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize'), \n  measurement, \n  avg(\"valueOne\") as \"valueOne\",\n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize'), \n  measurement || ' - value two' as metric, \n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1049,14 +1043,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1227,14 +1215,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1405,14 +1387,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1581,14 +1557,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1762,14 +1732,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -1943,14 +1907,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -2124,14 +2082,8 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
           "refId": "A"
-        },
-        {
-          "alias": "",
-          "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
-          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -2268,7 +2220,10 @@
   "refresh": false,
   "schemaVersion": 16,
   "style": "dark",
-  "tags": ["gdev", "postgres"],
+  "tags": [
+    "gdev",
+    "postgres"
+  ],
   "templating": {
     "list": [
       {
@@ -2397,5 +2352,5 @@
   "timezone": "",
   "title": "Datasource tests - Postgres (unittest)",
   "uid": "vHQdlVziz",
-  "version": 17
+  "version": 1
 }
\ No newline at end of file

From 8a22129177a8f3656cd55b411245d516a16c4c87 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Mon, 30 Jul 2018 14:37:23 +0200
Subject: [PATCH 145/380] add version note to metric prefix and fix typo

---
 docs/sources/features/datasources/mssql.md    | 3 ++-
 docs/sources/features/datasources/mysql.md    | 3 ++-
 docs/sources/features/datasources/postgres.md | 3 ++-
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
index bcb965dda74..ea7be8e1c30 100644
--- a/docs/sources/features/datasources/mssql.md
+++ b/docs/sources/features/datasources/mssql.md
@@ -148,7 +148,8 @@ The resulting table panel:
 
 ## Time series queries
 
-If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name.
+If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, the name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
 
 **Example database table:**
 
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index c6e620eb08b..22287b2a838 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -103,7 +103,8 @@ The resulting table panel:
 
 If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
 Any column except `time` and `metric` is treated as a value column.
-You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name.
+You may return a column named `metric` that is used as metric name for the value column.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
 
 **Example with `metric` column:**
 
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index f3e52ed6652..793b3b6f4c0 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -101,7 +101,8 @@ The resulting table panel:
 
 If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch.
 Any column except `time` and `metric` is treated as a value column.
-You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name.
+You may return a column named `metric` that is used as metric name for the value column.
+If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
 
 **Example with `metric` column:**
 

From 9c0fbe5a0b3c2e334cff6d6bbe2cb4d5ae48a5fd Mon Sep 17 00:00:00 2001
From: Worty <6840978+Worty@users.noreply.github.com>
Date: Mon, 30 Jul 2018 16:19:31 +0200
Subject: [PATCH 146/380] fixed that missing one

---
 public/app/core/utils/kbn.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts
index 74ef2a9e874..7bf2cdc5fd6 100644
--- a/public/app/core/utils/kbn.ts
+++ b/public/app/core/utils/kbn.ts
@@ -1118,7 +1118,7 @@ kbn.getUnitFormats = function() {
       submenu: [
         { text: 'parts-per-million (ppm)', value: 'ppm' },
         { text: 'parts-per-billion (ppb)', value: 'conppb' },
-        { text: 'nanogram per cubic metre (ng/m3)', value: 'conngm3' },
+        { text: 'nanogram per cubic metre (ng/m³)', value: 'conngm3' },
         { text: 'nanogram per normal cubic metre (ng/Nm³)', value: 'conngNm3' },
         { text: 'microgram per cubic metre (μg/m³)', value: 'conμgm3' },
         { text: 'microgram per normal cubic metre (μg/Nm³)', value: 'conμgNm3' },

From 4fa979649cf412c491a1d9d42d1d0062b13ff55d Mon Sep 17 00:00:00 2001
From: Worty <6840978+Worty@users.noreply.github.com>
Date: Mon, 30 Jul 2018 16:28:19 +0200
Subject: [PATCH 147/380] also fixed "Watt per square metre"

---
 public/app/core/utils/kbn.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts
index 7bf2cdc5fd6..c2764670b95 100644
--- a/public/app/core/utils/kbn.ts
+++ b/public/app/core/utils/kbn.ts
@@ -500,7 +500,7 @@ kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W');
 kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1);
 kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1);
 kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1);
-kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m2');
+kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m²');
 kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA');
 kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1);
 kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var');
@@ -1021,7 +1021,7 @@ kbn.getUnitFormats = function() {
         { text: 'Watt (W)', value: 'watt' },
         { text: 'Kilowatt (kW)', value: 'kwatt' },
         { text: 'Milliwatt (mW)', value: 'mwatt' },
-        { text: 'Watt per square metre (W/m2)', value: 'Wm2' },
+        { text: 'Watt per square metre (W/m²)', value: 'Wm2' },
         { text: 'Volt-ampere (VA)', value: 'voltamp' },
         { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' },
         { text: 'Volt-ampere reactive (var)', value: 'voltampreact' },

From 88d8072be3cd17ee7461481f1c17c51e69ed36b3 Mon Sep 17 00:00:00 2001
From: Jason Pereira <mindriot88@users.noreply.github.com>
Date: Mon, 30 Jul 2018 15:51:15 +0100
Subject: [PATCH 148/380] add aws_dx to cloudwatch datasource

---
 pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index 136ee241c2e..d2bd135ecc9 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -46,6 +46,7 @@ func init() {
 		"AWS/CloudFront":     {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
 		"AWS/CloudSearch":    {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
 		"AWS/DMS":            {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
+		"AWS/DX":             {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
 		"AWS/DynamoDB":       {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
 		"AWS/EBS":            {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps", "BurstBalance"},
 		"AWS/EC2":            {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
@@ -118,6 +119,7 @@ func init() {
 		"AWS/CloudFront":       {"DistributionId", "Region"},
 		"AWS/CloudSearch":      {},
 		"AWS/DMS":              {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
+		"AWS/DX":               {"ConnectionId"},
 		"AWS/DynamoDB":         {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
 		"AWS/EBS":              {"VolumeId"},
 		"AWS/EC2":              {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},

From 162d3e8036f8365e294502b6dcd496518c951a5b Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 30 Jul 2018 17:03:01 +0200
Subject: [PATCH 149/380] changelog: add notes about closing #12727

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8f5bced972..c2e8c5c788e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
 * **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)

From ad84a145f56f1fc1a8d513014c05ef40326f89a4 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 30 Jul 2018 17:03:24 +0200
Subject: [PATCH 150/380] changelog: add notes about closing #12744

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2e8c5c788e..11baca97714 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
+* **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
 
 # 5.2.2 (2018-07-25)
 

From e4c2476f3c898879fa6be89c18e1ea325bf88c13 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 31 Jul 2018 09:35:08 +0200
Subject: [PATCH 151/380] Weird execution order for the tests...

---
 .../datasource/prometheus/datasource.ts       |  7 +++++-
 .../prometheus/result_transformer.ts          |  7 +++++-
 .../prometheus/specs/_datasource.jest.ts      | 25 +++----------------
 3 files changed, 15 insertions(+), 24 deletions(-)

diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 75a946d6f36..6801a9a1d59 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -175,8 +175,12 @@ export class PrometheusDatasource {
           responseIndex: index,
           refId: activeTargets[index].refId,
         };
-
+        console.log('format: ' + transformerOptions.format);
+        console.log('resultType: ' + response.data.data.resultType);
+        console.log('legendFormat: ' + transformerOptions.legendFormat);
+        // console.log(result);
         this.resultTransformer.transform(result, response, transformerOptions);
+        // console.log(result);
       });
 
       return { data: result };
@@ -233,6 +237,7 @@ export class PrometheusDatasource {
     if (start > end) {
       throw { message: 'Invalid time range' };
     }
+    // console.log(query.expr);
 
     var url = '/api/v1/query_range';
     var data = {
diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts
index b6d8a32af5f..4b69cb98c54 100644
--- a/public/app/plugins/datasource/prometheus/result_transformer.ts
+++ b/public/app/plugins/datasource/prometheus/result_transformer.ts
@@ -6,7 +6,9 @@ export class ResultTransformer {
 
   transform(result: any, response: any, options: any) {
     let prometheusResult = response.data.data.result;
-
+    console.log(prometheusResult);
+    // console.log(options);
+    // console.log(result);
     if (options.format === 'table') {
       result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId));
     } else if (options.format === 'heatmap') {
@@ -26,6 +28,7 @@ export class ResultTransformer {
         }
       }
     }
+    // console.log(result);
   }
 
   transformMetricData(metricData, options, start, end) {
@@ -137,6 +140,7 @@ export class ResultTransformer {
     if (!label || label === '{}') {
       label = options.query;
     }
+    console.log(label);
     return label;
   }
 
@@ -156,6 +160,7 @@ export class ResultTransformer {
     var labelPart = _.map(_.toPairs(labelData), function(label) {
       return label[0] + '="' + label[1] + '"';
     }).join(',');
+    console.log(metricName);
     return metricName + '{' + labelPart + '}';
   }
 
diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
index 384abc8f902..34f78585d76 100644
--- a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
@@ -21,23 +21,7 @@ let backendSrv = <any>{
 };
 
 let templateSrv = {
-  replace: (target, scopedVars, format) => {
-    if (!target) {
-      return target;
-    }
-    let variable, value, fmt;
-
-    return target.replace(scopedVars, (match, var1, var2, fmt2, var3, fmt3) => {
-      variable = this.index[var1 || var2 || var3];
-      fmt = fmt2 || fmt3 || format;
-      if (scopedVars) {
-        value = scopedVars[var1 || var2 || var3];
-        if (value) {
-          return this.formatValue(value.value, fmt, variable);
-        }
-      }
-    });
-  },
+  replace: jest.fn(str => str),
 };
 
 let timeSrv = {
@@ -63,10 +47,7 @@ describe('PrometheusDatasource', function() {
   //     })
   //   );
 
-  beforeEach(() => {
-    ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-  });
-  describe('When querying prometheus with one target using query editor target spec', function() {
+  describe('When querying prometheus with one target using query editor target spec', async () => {
     var results;
     var query = {
       range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
@@ -106,7 +87,7 @@ describe('PrometheusDatasource', function() {
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
     });
-    it('should return series list', function() {
+    it('should return series list', async () => {
       expect(results.data.length).toBe(1);
       expect(results.data[0].target).toBe('test{job="testjob"}');
     });

From f1f0400769f01c99101914cb1ba62cca0e64ac94 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 31 Jul 2018 11:41:58 +0200
Subject: [PATCH 152/380] changelog: add notes about closing #12300

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11baca97714..d3532ebe640 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
+* **Cloudwatch**: AWS/AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)

From 7b5b94607b2956ab81d05c34fbb4c2e2fc615ab7 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Tue, 31 Jul 2018 12:51:07 +0200
Subject: [PATCH 153/380] fixed color for links in colored cells by adding a
 new variable that sets color: white when cell or row has background-color

---
 public/app/plugins/panel/table/renderer.ts | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts
index f6950dada52..456dadf6241 100644
--- a/public/app/plugins/panel/table/renderer.ts
+++ b/public/app/plugins/panel/table/renderer.ts
@@ -214,15 +214,20 @@ export class TableRenderer {
     var style = '';
     var cellClasses = [];
     var cellClass = '';
+    var linkStyle = '';
+
+    if (this.colorState.row) {
+      linkStyle = ' style="color: white"';
+    }
 
     if (this.colorState.cell) {
       style = ' style="background-color:' + this.colorState.cell + ';color: white"';
+      linkStyle = ' style="color: white;"';
       this.colorState.cell = null;
     } else if (this.colorState.value) {
       style = ' style="color:' + this.colorState.value + '"';
       this.colorState.value = null;
     }
-
     // because of the fixed table headers css only solution
     // there is an issue if header cell is wider the cell
     // this hack adds header content to cell (not visible)
@@ -253,7 +258,7 @@ export class TableRenderer {
 
       cellClasses.push('table-panel-cell-link');
       columnHtml += `
-        <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right">
+        <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right" ${linkStyle}>
           ${value}
         </a>
       `;

From 4b8ec4e32330b9fd606acc39604a8b9de7229ac3 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Tue, 31 Jul 2018 13:07:43 +0200
Subject: [PATCH 154/380] removed a blank space in div

---
 public/app/plugins/panel/table/renderer.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts
index 456dadf6241..c1e4e6243f9 100644
--- a/public/app/plugins/panel/table/renderer.ts
+++ b/public/app/plugins/panel/table/renderer.ts
@@ -258,7 +258,7 @@ export class TableRenderer {
 
       cellClasses.push('table-panel-cell-link');
       columnHtml += `
-        <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right" ${linkStyle}>
+        <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${linkStyle}>
           ${value}
         </a>
       `;

From 276a5e6eb5603df07d48aa66af4763bc9f3576c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 31 Jul 2018 17:29:02 +0200
Subject: [PATCH 155/380] fix: test data api route used old name for test data
 datasource, fixes #12773

---
 pkg/api/metrics.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go
index 00ad25ab8c2..f2bc79df7ad 100644
--- a/pkg/api/metrics.go
+++ b/pkg/api/metrics.go
@@ -99,7 +99,7 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response {
 	timeRange := tsdb.NewTimeRange(from, to)
 	request := &tsdb.TsdbQuery{TimeRange: timeRange}
 
-	dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"}
+	dsInfo := &m.DataSource{Type: "testdata"}
 	request.Queries = append(request.Queries, &tsdb.Query{
 		RefId:      "A",
 		IntervalMs: intervalMs,

From 89eae1566d036e153aea18eb62e983bc21bd315f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 31 Jul 2018 17:31:45 +0200
Subject: [PATCH 156/380] fix: team email tooltip was not showing

---
 public/app/core/components/Forms/Forms.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/core/components/Forms/Forms.tsx b/public/app/core/components/Forms/Forms.tsx
index 4b74d48ba08..543e1a1d6df 100644
--- a/public/app/core/components/Forms/Forms.tsx
+++ b/public/app/core/components/Forms/Forms.tsx
@@ -12,7 +12,7 @@ export const Label: SFC<Props> = props => {
     <span className="gf-form-label width-10">
       <span>{props.children}</span>
       {props.tooltip && (
-        <Tooltip className="gf-form-help-icon--right-normal" placement="auto" content="hello">
+        <Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>
           <i className="gicon gicon-question gicon--has-hover" />
         </Tooltip>
       )}

From 6df3722a35faf455e2d25989a80a8e167531b5b7 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 31 Jul 2018 18:01:36 +0200
Subject: [PATCH 157/380] changelog: add notes about closing #12762

[skip ci]
---
 CHANGELOG.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3532ebe640..dde7ead6f13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,7 +23,8 @@
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
-* **Cloudwatch**: AWS/AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)

From 43295f9c189e5fd5539892162562be7d33046603 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Mon, 30 Jul 2018 13:23:29 +0200
Subject: [PATCH 158/380] remove alias from postgres $__timeGroup macro

---
 docs/sources/features/datasources/postgres.md               | 2 +-
 pkg/tsdb/postgres/macros.go                                 | 2 +-
 pkg/tsdb/postgres/macros_test.go                            | 4 ++--
 pkg/tsdb/postgres/postgres_test.go                          | 6 +++---
 .../plugins/datasource/postgres/partials/query.editor.html  | 2 +-
 5 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index 793b3b6f4c0..7915f29fcdc 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -60,7 +60,7 @@ Macro example | Description
 *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
-*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time*
+*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300*
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 661dbf3d4ce..852e9d7997e 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -109,7 +109,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 				m.query.Model.Set("fillValue", floatVal)
 			}
 		}
-		return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
+		return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index 194573be0fd..bb947d4f01f 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -53,7 +53,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time")
+				So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300")
 			})
 
 			Convey("interpolate __timeGroup function with spaces between args", func() {
@@ -61,7 +61,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time")
+				So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300")
 			})
 
 			Convey("interpolate __timeTo function", func() {
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index c7787929a9d..3e864dca1e6 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -183,7 +183,7 @@ func TestPostgres(t *testing.T) {
 					Queries: []*tsdb.Query{
 						{
 							Model: simplejson.NewFromAny(map[string]interface{}{
-								"rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
 							}),
 							RefId: "A",
@@ -227,7 +227,7 @@ func TestPostgres(t *testing.T) {
 					Queries: []*tsdb.Query{
 						{
 							Model: simplejson.NewFromAny(map[string]interface{}{
-								"rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
 							}),
 							RefId: "A",
@@ -281,7 +281,7 @@ func TestPostgres(t *testing.T) {
 					Queries: []*tsdb.Query{
 						{
 							Model: simplejson.NewFromAny(map[string]interface{}{
-								"rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
 							}),
 							RefId: "A",
diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html
index b7c12471f52..1ace05abae2 100644
--- a/public/app/plugins/datasource/postgres/partials/query.editor.html
+++ b/public/app/plugins/datasource/postgres/partials/query.editor.html
@@ -53,7 +53,7 @@ Macros:
 - $__timeEpoch -&gt; extract(epoch from column) as "time"
 - $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
 - $__unixEpochFilter(column) -&gt;  column &gt;= 1492750877 AND column &lt;= 1492750877
-- $__timeGroup(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300 AS time
+- $__timeGroup(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300
 
 Example of group by and order by with $__timeGroup:
 SELECT

From bd77541e092e022166a265d81e26583f6305de14 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Wed, 1 Aug 2018 08:00:43 +0200
Subject: [PATCH 159/380] adjust test dashboards

---
 .../datasource_tests_postgres_unittest.json   | 20 +++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
index 2243baed0aa..a3139bf99f7 100644
--- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
@@ -369,7 +369,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -452,7 +452,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -535,7 +535,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -618,7 +618,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -701,7 +701,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -784,7 +784,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -871,7 +871,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize'), \n  measurement, \n  avg(\"valueOne\") as \"valueOne\",\n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroupAlias(time, '$summarize'), \n  measurement, \n  avg(\"valueOne\") as \"valueOne\",\n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
           "refId": "A"
         }
       ],
@@ -956,7 +956,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize'), \n  avg(\"valueOne\") as \"valueOne\", \n  avg(\"valueTwo\") as \"valueTwo\" \nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') AS time, \n  avg(\"valueOne\") as \"valueOne\", \n  avg(\"valueTwo\") as \"valueTwo\" \nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
           "refId": "A"
         }
       ],
@@ -2352,5 +2352,5 @@
   "timezone": "",
   "title": "Datasource tests - Postgres (unittest)",
   "uid": "vHQdlVziz",
-  "version": 1
-}
\ No newline at end of file
+  "version": 17
+}

From 42f189282618fb5ce42efc7f8cf804bdb1da65da Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Wed, 1 Aug 2018 08:48:22 +0200
Subject: [PATCH 160/380] Add $__timeGroupAlias to postgres macros

---
 .../datasource_tests_postgres_unittest.json     | 17 +++++++++--------
 pkg/tsdb/postgres/macros.go                     |  6 ++++++
 pkg/tsdb/postgres/macros_test.go                | 14 ++++++++++----
 3 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
index a3139bf99f7..3c2b34df78c 100644
--- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
@@ -369,7 +369,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -452,7 +452,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -535,7 +535,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -618,7 +618,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -701,7 +701,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -784,7 +784,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -956,7 +956,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') AS time, \n  avg(\"valueOne\") as \"valueOne\", \n  avg(\"valueTwo\") as \"valueTwo\" \nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroupAlias(time, '$summarize'), \n  avg(\"valueOne\") as \"valueOne\", \n  avg(\"valueTwo\") as \"valueTwo\" \nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
           "refId": "A"
         }
       ],
@@ -2352,5 +2352,6 @@
   "timezone": "",
   "title": "Datasource tests - Postgres (unittest)",
   "uid": "vHQdlVziz",
-  "version": 17
+  "version": 1
 }
+
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 852e9d7997e..fa887032c5d 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -110,6 +110,12 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			}
 		}
 		return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__timeGroupAlias":
+		tg, err := m.evaluateMacro("__timeGroup", args)
+		if err == nil {
+			return tg + " AS \"time\"", err
+		}
+		return "", err
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index bb947d4f01f..ec74470a803 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -50,18 +50,24 @@ func TestMacroEngine(t *testing.T) {
 
 			Convey("interpolate __timeGroup function", func() {
 
-				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+				sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column,'5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300")
+				So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300")
+				So(sql2, ShouldEqual, sql+" AS \"time\"")
 			})
 
 			Convey("interpolate __timeGroup function with spaces between args", func() {
 
-				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+				sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column , '5m')")
+				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column , '5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300")
+				So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300")
+				So(sql2, ShouldEqual, sql+" AS \"time\"")
 			})
 
 			Convey("interpolate __timeTo function", func() {

From d4d896ade829300fa306bac82798d746a85e9693 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Wed, 1 Aug 2018 09:08:17 +0200
Subject: [PATCH 161/380] replaced style with class for links

---
 public/app/plugins/panel/table/renderer.ts          | 13 +++++++++----
 .../app/plugins/panel/table/specs/renderer.jest.ts  |  2 +-
 public/sass/components/_panel_table.scss            |  4 ++++
 3 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts
index c1e4e6243f9..474e9c89493 100644
--- a/public/app/plugins/panel/table/renderer.ts
+++ b/public/app/plugins/panel/table/renderer.ts
@@ -214,15 +214,15 @@ export class TableRenderer {
     var style = '';
     var cellClasses = [];
     var cellClass = '';
-    var linkStyle = '';
+    var linkClass = '';
 
     if (this.colorState.row) {
-      linkStyle = ' style="color: white"';
+      linkClass = 'table-panel-link';
     }
 
     if (this.colorState.cell) {
       style = ' style="background-color:' + this.colorState.cell + ';color: white"';
-      linkStyle = ' style="color: white;"';
+      linkClass = 'table-panel-link';
       this.colorState.cell = null;
     } else if (this.colorState.value) {
       style = ' style="color:' + this.colorState.value + '"';
@@ -258,7 +258,12 @@ export class TableRenderer {
 
       cellClasses.push('table-panel-cell-link');
       columnHtml += `
-        <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${linkStyle}>
+        <a href="${cellLink}"
+           target="${cellTarget}"
+           data-link-tooltip
+           data-original-title="${cellLinkTooltip}"
+           data-placement="right"
+           class="${linkClass}">
           ${value}
         </a>
       `;
diff --git a/public/app/plugins/panel/table/specs/renderer.jest.ts b/public/app/plugins/panel/table/specs/renderer.jest.ts
index 22957d1aa66..f1a686fb739 100644
--- a/public/app/plugins/panel/table/specs/renderer.jest.ts
+++ b/public/app/plugins/panel/table/specs/renderer.jest.ts
@@ -268,7 +268,7 @@ describe('when rendering table', () => {
       var expectedHtml = `
         <td class="table-panel-cell-link">
           <a href="/dashboard?param=host1&param_1=1230&param_2=40"
-            target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right">
+            target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right" class="">
             host1
           </a>
         </td>
diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss
index 8e0ecf15896..99e91f8ff67 100644
--- a/public/sass/components/_panel_table.scss
+++ b/public/sass/components/_panel_table.scss
@@ -133,3 +133,7 @@
   height: 0px;
   line-height: 0px;
 }
+
+.table-panel-link {
+  color: white;
+}

From d6158bc2935ec396f45114d736e684bb3a522c6b Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 09:30:26 +0200
Subject: [PATCH 162/380] All tests passing

---
 .../datasource/prometheus/datasource.ts       |   6 -
 .../prometheus/result_transformer.ts          |   7 +-
 .../prometheus/specs/_datasource.jest.ts      | 333 +++++----
 .../prometheus/specs/datasource_specs.ts      | 683 ------------------
 4 files changed, 196 insertions(+), 833 deletions(-)
 delete mode 100644 public/app/plugins/datasource/prometheus/specs/datasource_specs.ts

diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 6801a9a1d59..ac8d774db59 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -175,12 +175,7 @@ export class PrometheusDatasource {
           responseIndex: index,
           refId: activeTargets[index].refId,
         };
-        console.log('format: ' + transformerOptions.format);
-        console.log('resultType: ' + response.data.data.resultType);
-        console.log('legendFormat: ' + transformerOptions.legendFormat);
-        // console.log(result);
         this.resultTransformer.transform(result, response, transformerOptions);
-        // console.log(result);
       });
 
       return { data: result };
@@ -237,7 +232,6 @@ export class PrometheusDatasource {
     if (start > end) {
       throw { message: 'Invalid time range' };
     }
-    // console.log(query.expr);
 
     var url = '/api/v1/query_range';
     var data = {
diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts
index 4b69cb98c54..b6d8a32af5f 100644
--- a/public/app/plugins/datasource/prometheus/result_transformer.ts
+++ b/public/app/plugins/datasource/prometheus/result_transformer.ts
@@ -6,9 +6,7 @@ export class ResultTransformer {
 
   transform(result: any, response: any, options: any) {
     let prometheusResult = response.data.data.result;
-    console.log(prometheusResult);
-    // console.log(options);
-    // console.log(result);
+
     if (options.format === 'table') {
       result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId));
     } else if (options.format === 'heatmap') {
@@ -28,7 +26,6 @@ export class ResultTransformer {
         }
       }
     }
-    // console.log(result);
   }
 
   transformMetricData(metricData, options, start, end) {
@@ -140,7 +137,6 @@ export class ResultTransformer {
     if (!label || label === '{}') {
       label = options.query;
     }
-    console.log(label);
     return label;
   }
 
@@ -160,7 +156,6 @@ export class ResultTransformer {
     var labelPart = _.map(_.toPairs(labelData), function(label) {
       return label[0] + '="' + label[1] + '"';
     }).join(',');
-    console.log(metricName);
     return metricName + '{' + labelPart + '}';
   }
 
diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
index 34f78585d76..2deab13a101 100644
--- a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
@@ -1,6 +1,7 @@
 import moment from 'moment';
 import { PrometheusDatasource } from '../datasource';
 import $q from 'q';
+import { angularMocks } from 'test/lib/common';
 
 const SECOND = 1000;
 const MINUTE = 60 * SECOND;
@@ -57,32 +58,31 @@ describe('PrometheusDatasource', function() {
     // Interval alignment with step
     var urlExpected =
       'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60';
-    var response = {
-      data: {
-        status: 'success',
-        data: {
-          resultType: 'matrix',
-          result: [
-            {
-              metric: { __name__: 'test', job: 'testjob' },
-              values: [[60, '3846']],
-            },
-          ],
-        },
-      },
-    };
+
     beforeEach(async () => {
-      //   ctx.$httpBackend.expect('GET', urlExpected).respond(response);
+      let response = {
+        data: {
+          status: 'success',
+          data: {
+            resultType: 'matrix',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob' },
+                values: [[60, '3846']],
+              },
+            ],
+          },
+        },
+      };
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
 
       await ctx.ds.query(query).then(function(data) {
         results = data;
       });
-      //   ctx.$httpBackend.flush();
     });
+
     it('should generate the correct query', function() {
-      //   ctx.$httpBackend.verifyNoOutstandingExpectation();
       let res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
@@ -97,39 +97,33 @@ describe('PrometheusDatasource', function() {
     var start = 60;
     var end = 360;
     var step = 60;
-    // var urlExpected =
-    //   'proxied/api/v1/query_range?query=' +
-    //   encodeURIComponent('test{job="testjob"}') +
-    //   '&start=' +
-    //   start +
-    //   '&end=' +
-    //   end +
-    //   '&step=' +
-    //   step;
+
     var query = {
       range: { from: time({ seconds: start }), to: time({ seconds: end }) },
       targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
       interval: '60s',
     };
-    var response = {
-      status: 'success',
-      data: {
-        data: {
-          resultType: 'matrix',
-          result: [
-            {
-              metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
-              values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']],
-            },
-            {
-              metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
-              values: [[start + step * 2, '4846']],
-            },
-          ],
-        },
-      },
-    };
+
     beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'matrix',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
+                values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']],
+              },
+              {
+                metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
+                values: [[start + step * 2, '4846']],
+              },
+            ],
+          },
+        },
+      };
+
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
 
@@ -137,11 +131,13 @@ describe('PrometheusDatasource', function() {
         results = data;
       });
     });
+
     it('should be same length', function() {
       expect(results.data.length).toBe(2);
       expect(results.data[0].datapoints.length).toBe((end - start) / step + 1);
       expect(results.data[1].datapoints.length).toBe((end - start) / step + 1);
     });
+
     it('should fill null until first datapoint in response', function() {
       expect(results.data[0].datapoints[0][1]).toBe(start * 1000);
       expect(results.data[0].datapoints[0][0]).toBe(null);
@@ -172,21 +168,23 @@ describe('PrometheusDatasource', function() {
       targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
       interval: '60s',
     };
-    var response = {
-      status: 'success',
-      data: {
-        data: {
-          resultType: 'vector',
-          result: [
-            {
-              metric: { __name__: 'test', job: 'testjob' },
-              value: [123, '3846'],
-            },
-          ],
-        },
-      },
-    };
+
     beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'vector',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob' },
+                value: [123, '3846'],
+              },
+            ],
+          },
+        },
+      };
+
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
 
@@ -206,10 +204,7 @@ describe('PrometheusDatasource', function() {
   });
   describe('When performing annotationQuery', function() {
     var results;
-    // var urlExpected =
-    //   'proxied/api/v1/query_range?query=' +
-    //   encodeURIComponent('ALERTS{alertstate="firing"}') +
-    //   '&start=60&end=180&step=60';
+
     var options = {
       annotation: {
         expr: 'ALERTS{alertstate="firing"}',
@@ -222,27 +217,29 @@ describe('PrometheusDatasource', function() {
         to: time({ seconds: 123 }),
       },
     };
-    var response = {
-      status: 'success',
-      data: {
-        data: {
-          resultType: 'matrix',
-          result: [
-            {
-              metric: {
-                __name__: 'ALERTS',
-                alertname: 'InstanceDown',
-                alertstate: 'firing',
-                instance: 'testinstance',
-                job: 'testjob',
-              },
-              values: [[123, '1']],
-            },
-          ],
-        },
-      },
-    };
+
     beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'matrix',
+            result: [
+              {
+                metric: {
+                  __name__: 'ALERTS',
+                  alertname: 'InstanceDown',
+                  alertstate: 'firing',
+                  instance: 'testinstance',
+                  job: 'testjob',
+                },
+                values: [[123, '1']],
+              },
+            ],
+          },
+        },
+      };
+
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
 
@@ -262,28 +259,29 @@ describe('PrometheusDatasource', function() {
 
   describe('When resultFormat is table and instant = true', function() {
     var results;
-    var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
+    // var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
     var query = {
       range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
       targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
       interval: '60s',
     };
-    var response = {
-      status: 'success',
-      data: {
-        data: {
-          resultType: 'vector',
-          result: [
-            {
-              metric: { __name__: 'test', job: 'testjob' },
-              value: [123, '3846'],
-            },
-          ],
-        },
-      },
-    };
 
     beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'vector',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob' },
+                value: [123, '3846'],
+              },
+            ],
+          },
+        },
+      };
+
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
       await ctx.ds.query(query).then(function(data) {
@@ -520,9 +518,13 @@ describe('PrometheusDatasource', function() {
           __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
         },
       };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10';
 
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=60&end=420&step=10';
+
+      templateSrv.replace = jest.fn(str => str);
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
       await ctx.ds.query(query);
@@ -530,10 +532,16 @@ describe('PrometheusDatasource', function() {
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
 
-      expect(query.scopedVars.__interval.text).toBe('10s');
-      expect(query.scopedVars.__interval.value).toBe('10s');
-      expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
-      expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000);
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '10s',
+          value: '10s',
+        },
+        __interval_ms: {
+          text: 10000,
+          value: 10000,
+        },
+      });
     });
     it('should be min interval when it is greater than auto interval', async () => {
       var query = {
@@ -552,18 +560,27 @@ describe('PrometheusDatasource', function() {
         },
       };
       var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10';
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=60&end=420&step=10';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      templateSrv.replace = jest.fn(str => str);
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
       await ctx.ds.query(query);
       let res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
 
-      expect(query.scopedVars.__interval.text).toBe('5s');
-      expect(query.scopedVars.__interval.value).toBe('5s');
-      expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000);
-      expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000);
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '5s',
+          value: '5s',
+        },
+        __interval_ms: {
+          text: 5000,
+          value: 5000,
+        },
+      });
     });
     it('should account for intervalFactor', async () => {
       var query = {
@@ -583,14 +600,28 @@ describe('PrometheusDatasource', function() {
         },
       };
       var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[100s])') + '&start=0&end=500&step=100';
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=0&end=500&step=100';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      templateSrv.replace = jest.fn(str => str);
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
       await ctx.ds.query(query);
       let res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
 
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '10s',
+          value: '10s',
+        },
+        __interval_ms: {
+          text: 10000,
+          value: 10000,
+        },
+      });
+
       expect(query.scopedVars.__interval.text).toBe('10s');
       expect(query.scopedVars.__interval.value).toBe('10s');
       expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
@@ -614,7 +645,11 @@ describe('PrometheusDatasource', function() {
         },
       };
       var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[50s])') + '&start=50&end=450&step=50';
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=50&end=450&step=50';
+
+      templateSrv.replace = jest.fn(str => str);
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
       await ctx.ds.query(query);
@@ -622,10 +657,16 @@ describe('PrometheusDatasource', function() {
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
 
-      expect(query.scopedVars.__interval.text).toBe('5s');
-      expect(query.scopedVars.__interval.value).toBe('5s');
-      expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000);
-      expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000);
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '5s',
+          value: '5s',
+        },
+        __interval_ms: {
+          text: 5000,
+          value: 5000,
+        },
+      });
     });
     it('should be min interval when greater than interval * intervalFactor', async () => {
       var query = {
@@ -645,7 +686,9 @@ describe('PrometheusDatasource', function() {
         },
       };
       var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[15s])') + '&start=60&end=420&step=15';
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=60&end=420&step=15';
 
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
@@ -654,10 +697,16 @@ describe('PrometheusDatasource', function() {
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
 
-      expect(query.scopedVars.__interval.text).toBe('5s');
-      expect(query.scopedVars.__interval.value).toBe('5s');
-      expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000);
-      expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000);
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '5s',
+          value: '5s',
+        },
+        __interval_ms: {
+          text: 5000,
+          value: 5000,
+        },
+      });
     });
     it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => {
       var query = {
@@ -679,23 +728,30 @@ describe('PrometheusDatasource', function() {
       var start = 0;
       var urlExpected =
         'proxied/api/v1/query_range?query=' +
-        encodeURIComponent('rate(test[60s])') +
+        encodeURIComponent('rate(test[$__interval])') +
         '&start=' +
         start +
         '&end=' +
         end +
         '&step=60';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      templateSrv.replace = jest.fn(str => str);
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
       await ctx.ds.query(query);
       let res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
 
-      expect(query.scopedVars.__interval.text).toBe('5s');
-      expect(query.scopedVars.__interval.value).toBe('5s');
-      expect(query.scopedVars.__interval_ms.text).toBe(5 * 1000);
-      expect(query.scopedVars.__interval_ms.value).toBe(5 * 1000);
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '5s',
+          value: '5s',
+        },
+        __interval_ms: {
+          text: 5000,
+          value: 5000,
+        },
+      });
     });
   });
 });
@@ -738,21 +794,22 @@ describe('PrometheusDatasource for POST', function() {
       targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
       interval: '60s',
     };
-    var response = {
-      status: 'success',
-      data: {
-        data: {
-          resultType: 'matrix',
-          result: [
-            {
-              metric: { __name__: 'test', job: 'testjob' },
-              values: [[2 * 60, '3846']],
-            },
-          ],
-        },
-      },
-    };
+
     beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'matrix',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob' },
+                values: [[2 * 60, '3846']],
+              },
+            ],
+          },
+        },
+      };
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
       await ctx.ds.query(query).then(function(data) {
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
deleted file mode 100644
index c5da671b757..00000000000
--- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
+++ /dev/null
@@ -1,683 +0,0 @@
-import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
-import moment from 'moment';
-import $ from 'jquery';
-import helpers from 'test/specs/helpers';
-import { PrometheusDatasource } from '../datasource';
-
-const SECOND = 1000;
-const MINUTE = 60 * SECOND;
-const HOUR = 60 * MINUTE;
-
-const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND);
-
-describe('PrometheusDatasource', function() {
-  var ctx = new helpers.ServiceTestContext();
-  var instanceSettings = {
-    url: 'proxied',
-    directUrl: 'direct',
-    user: 'test',
-    password: 'mupp',
-    jsonData: { httpMethod: 'GET' },
-  };
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(ctx.providePhase(['timeSrv']));
-
-  beforeEach(
-    angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
-      ctx.$q = $q;
-      ctx.$httpBackend = $httpBackend;
-      ctx.$rootScope = $rootScope;
-      ctx.ds = $injector.instantiate(PrometheusDatasource, {
-        instanceSettings: instanceSettings,
-      });
-      $httpBackend.when('GET', /\.html$/).respond('');
-    })
-  );
-  describe('When querying prometheus with one target using query editor target spec', function() {
-    var results;
-    var query = {
-      range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
-      interval: '60s',
-    };
-    // Interval alignment with step
-    var urlExpected =
-      'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60';
-    var response = {
-      status: 'success',
-      data: {
-        resultType: 'matrix',
-        result: [
-          {
-            metric: { __name__: 'test', job: 'testjob' },
-            values: [[60, '3846']],
-          },
-        ],
-      },
-    };
-    beforeEach(function() {
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-      ctx.$httpBackend.flush();
-    });
-    it('should generate the correct query', function() {
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should return series list', function() {
-      expect(results.data.length).to.be(1);
-      expect(results.data[0].target).to.be('test{job="testjob"}');
-    });
-  });
-  describe('When querying prometheus with one target which return multiple series', function() {
-    var results;
-    var start = 60;
-    var end = 360;
-    var step = 60;
-    var urlExpected =
-      'proxied/api/v1/query_range?query=' +
-      encodeURIComponent('test{job="testjob"}') +
-      '&start=' +
-      start +
-      '&end=' +
-      end +
-      '&step=' +
-      step;
-    var query = {
-      range: { from: time({ seconds: start }), to: time({ seconds: end }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
-      interval: '60s',
-    };
-    var response = {
-      status: 'success',
-      data: {
-        resultType: 'matrix',
-        result: [
-          {
-            metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
-            values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']],
-          },
-          {
-            metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
-            values: [[start + step * 2, '4846']],
-          },
-        ],
-      },
-    };
-    beforeEach(function() {
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-      ctx.$httpBackend.flush();
-    });
-    it('should be same length', function() {
-      expect(results.data.length).to.be(2);
-      expect(results.data[0].datapoints.length).to.be((end - start) / step + 1);
-      expect(results.data[1].datapoints.length).to.be((end - start) / step + 1);
-    });
-    it('should fill null until first datapoint in response', function() {
-      expect(results.data[0].datapoints[0][1]).to.be(start * 1000);
-      expect(results.data[0].datapoints[0][0]).to.be(null);
-      expect(results.data[0].datapoints[1][1]).to.be((start + step * 1) * 1000);
-      expect(results.data[0].datapoints[1][0]).to.be(3846);
-    });
-    it('should fill null after last datapoint in response', function() {
-      var length = (end - start) / step + 1;
-      expect(results.data[0].datapoints[length - 2][1]).to.be((end - step * 1) * 1000);
-      expect(results.data[0].datapoints[length - 2][0]).to.be(3848);
-      expect(results.data[0].datapoints[length - 1][1]).to.be(end * 1000);
-      expect(results.data[0].datapoints[length - 1][0]).to.be(null);
-    });
-    it('should fill null at gap between series', function() {
-      expect(results.data[0].datapoints[2][1]).to.be((start + step * 2) * 1000);
-      expect(results.data[0].datapoints[2][0]).to.be(null);
-      expect(results.data[1].datapoints[1][1]).to.be((start + step * 1) * 1000);
-      expect(results.data[1].datapoints[1][0]).to.be(null);
-      expect(results.data[1].datapoints[3][1]).to.be((start + step * 3) * 1000);
-      expect(results.data[1].datapoints[3][0]).to.be(null);
-    });
-  });
-  describe('When querying prometheus with one target and instant = true', function() {
-    var results;
-    var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
-    var query = {
-      range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
-      interval: '60s',
-    };
-    var response = {
-      status: 'success',
-      data: {
-        resultType: 'vector',
-        result: [
-          {
-            metric: { __name__: 'test', job: 'testjob' },
-            value: [123, '3846'],
-          },
-        ],
-      },
-    };
-    beforeEach(function() {
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-      ctx.$httpBackend.flush();
-    });
-    it('should generate the correct query', function() {
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should return series list', function() {
-      expect(results.data.length).to.be(1);
-      expect(results.data[0].target).to.be('test{job="testjob"}');
-    });
-  });
-  describe('When performing annotationQuery', function() {
-    var results;
-    var urlExpected =
-      'proxied/api/v1/query_range?query=' +
-      encodeURIComponent('ALERTS{alertstate="firing"}') +
-      '&start=60&end=180&step=60';
-    var options = {
-      annotation: {
-        expr: 'ALERTS{alertstate="firing"}',
-        tagKeys: 'job',
-        titleFormat: '{{alertname}}',
-        textFormat: '{{instance}}',
-      },
-      range: {
-        from: time({ seconds: 63 }),
-        to: time({ seconds: 123 }),
-      },
-    };
-    var response = {
-      status: 'success',
-      data: {
-        resultType: 'matrix',
-        result: [
-          {
-            metric: {
-              __name__: 'ALERTS',
-              alertname: 'InstanceDown',
-              alertstate: 'firing',
-              instance: 'testinstance',
-              job: 'testjob',
-            },
-            values: [[123, '1']],
-          },
-        ],
-      },
-    };
-    beforeEach(function() {
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.annotationQuery(options).then(function(data) {
-        results = data;
-      });
-      ctx.$httpBackend.flush();
-    });
-    it('should return annotation list', function() {
-      ctx.$rootScope.$apply();
-      expect(results.length).to.be(1);
-      expect(results[0].tags).to.contain('testjob');
-      expect(results[0].title).to.be('InstanceDown');
-      expect(results[0].text).to.be('testinstance');
-      expect(results[0].time).to.be(123 * 1000);
-    });
-  });
-
-  describe('When resultFormat is table and instant = true', function() {
-    var results;
-    var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
-    var query = {
-      range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
-      interval: '60s',
-    };
-    var response = {
-      status: 'success',
-      data: {
-        resultType: 'vector',
-        result: [
-          {
-            metric: { __name__: 'test', job: 'testjob' },
-            value: [123, '3846'],
-          },
-        ],
-      },
-    };
-
-    beforeEach(function() {
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-      ctx.$httpBackend.flush();
-    });
-
-    it('should return result', () => {
-      expect(results).not.to.be(null);
-    });
-  });
-
-  describe('The "step" query parameter', function() {
-    var response = {
-      status: 'success',
-      data: {
-        resultType: 'matrix',
-        result: [],
-      },
-    };
-
-    it('should be min interval when greater than auto interval', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '10s',
-          },
-        ],
-        interval: '5s',
-      };
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-
-    it('step should never go below 1', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [{ expr: 'test' }],
-        interval: '100ms',
-      };
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-
-    it('should be auto interval when greater than min interval', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '5s',
-          },
-        ],
-        interval: '10s',
-      };
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should result in querying fewer than 11000 data points', function() {
-      var query = {
-        // 6 hour range
-        range: { from: time({ hours: 1 }), to: time({ hours: 7 }) },
-        targets: [{ expr: 'test' }],
-        interval: '1s',
-      };
-      var end = 7 * 60 * 60;
-      var start = 60 * 60;
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should not apply min interval when interval * intervalFactor greater', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '10s',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '5s',
-      };
-      // times get rounded up to interval
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should apply min interval when interval * intervalFactor smaller', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '15s',
-            intervalFactor: 2,
-          },
-        ],
-        interval: '5s',
-      };
-      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should apply intervalFactor to auto interval when greater', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '5s',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '10s',
-      };
-      // times get aligned to interval
-      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should not not be affected by the 11000 data points limit when large enough', function() {
-      var query = {
-        // 1 week range
-        range: { from: time({}), to: time({ hours: 7 * 24 }) },
-        targets: [
-          {
-            expr: 'test',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '10s',
-      };
-      var end = 7 * 24 * 60 * 60;
-      var start = 0;
-      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should be determined by the 11000 data points limit when too small', function() {
-      var query = {
-        // 1 week range
-        range: { from: time({}), to: time({ hours: 7 * 24 }) },
-        targets: [
-          {
-            expr: 'test',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '5s',
-      };
-      var end = 7 * 24 * 60 * 60;
-      var start = 0;
-      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-  });
-
-  describe('The __interval and __interval_ms template variables', function() {
-    var response = {
-      status: 'success',
-      data: {
-        resultType: 'matrix',
-        result: [],
-      },
-    };
-
-    it('should be unchanged when auto interval is greater than min interval', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '5s',
-          },
-        ],
-        interval: '10s',
-        scopedVars: {
-          __interval: { text: '10s', value: '10s' },
-          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-
-      expect(query.scopedVars.__interval.text).to.be('10s');
-      expect(query.scopedVars.__interval.value).to.be('10s');
-      expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
-      expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
-    });
-    it('should be min interval when it is greater than auto interval', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '10s',
-          },
-        ],
-        interval: '5s',
-        scopedVars: {
-          __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-
-      expect(query.scopedVars.__interval.text).to.be('5s');
-      expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
-      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
-    });
-    it('should account for intervalFactor', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '5s',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '10s',
-        scopedVars: {
-          __interval: { text: '10s', value: '10s' },
-          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[100s])') + '&start=0&end=500&step=100';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-
-      expect(query.scopedVars.__interval.text).to.be('10s');
-      expect(query.scopedVars.__interval.value).to.be('10s');
-      expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
-      expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
-    });
-    it('should be interval * intervalFactor when greater than min interval', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '10s',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '5s',
-        scopedVars: {
-          __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[50s])') + '&start=50&end=450&step=50';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-
-      expect(query.scopedVars.__interval.text).to.be('5s');
-      expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
-      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
-    });
-    it('should be min interval when greater than interval * intervalFactor', function() {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '15s',
-            intervalFactor: 2,
-          },
-        ],
-        interval: '5s',
-        scopedVars: {
-          __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[15s])') + '&start=60&end=420&step=15';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-
-      expect(query.scopedVars.__interval.text).to.be('5s');
-      expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
-      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
-    });
-    it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
-      var query = {
-        // 1 week range
-        range: { from: time({}), to: time({ hours: 7 * 24 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '5s',
-        scopedVars: {
-          __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
-        },
-      };
-      var end = 7 * 24 * 60 * 60;
-      var start = 0;
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' +
-        encodeURIComponent('rate(test[60s])') +
-        '&start=' +
-        start +
-        '&end=' +
-        end +
-        '&step=60';
-      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
-      ctx.ds.query(query);
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-
-      expect(query.scopedVars.__interval.text).to.be('5s');
-      expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
-      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
-    });
-  });
-});
-
-describe('PrometheusDatasource for POST', function() {
-  var ctx = new helpers.ServiceTestContext();
-  var instanceSettings = {
-    url: 'proxied',
-    directUrl: 'direct',
-    user: 'test',
-    password: 'mupp',
-    jsonData: { httpMethod: 'POST' },
-  };
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(ctx.providePhase(['timeSrv']));
-
-  beforeEach(
-    angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
-      ctx.$q = $q;
-      ctx.$httpBackend = $httpBackend;
-      ctx.$rootScope = $rootScope;
-      ctx.ds = $injector.instantiate(PrometheusDatasource, { instanceSettings: instanceSettings });
-      $httpBackend.when('GET', /\.html$/).respond('');
-    })
-  );
-
-  describe('When querying prometheus with one target using query editor target spec', function() {
-    var results;
-    var urlExpected = 'proxied/api/v1/query_range';
-    var dataExpected = $.param({
-      query: 'test{job="testjob"}',
-      start: 1 * 60,
-      end: 3 * 60,
-      step: 60,
-    });
-    var query = {
-      range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
-      interval: '60s',
-    };
-    var response = {
-      status: 'success',
-      data: {
-        resultType: 'matrix',
-        result: [
-          {
-            metric: { __name__: 'test', job: 'testjob' },
-            values: [[2 * 60, '3846']],
-          },
-        ],
-      },
-    };
-    beforeEach(function() {
-      ctx.$httpBackend.expectPOST(urlExpected, dataExpected).respond(response);
-      ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-      ctx.$httpBackend.flush();
-    });
-    it('should generate the correct query', function() {
-      ctx.$httpBackend.verifyNoOutstandingExpectation();
-    });
-    it('should return series list', function() {
-      expect(results.data.length).to.be(1);
-      expect(results.data[0].target).to.be('test{job="testjob"}');
-    });
-  });
-});

From 790aadf8ef3544eb0c1007042525c7ad54f611e2 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 10:09:05 +0200
Subject: [PATCH 163/380] Remove angularMocks

---
 .../app/plugins/datasource/prometheus/specs/_datasource.jest.ts  | 1 -
 1 file changed, 1 deletion(-)

diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
index 2deab13a101..efe2738cce9 100644
--- a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
@@ -1,7 +1,6 @@
 import moment from 'moment';
 import { PrometheusDatasource } from '../datasource';
 import $q from 'q';
-import { angularMocks } from 'test/lib/common';
 
 const SECOND = 1000;
 const MINUTE = 60 * SECOND;

From 8d0c4cdc09c04a05f20d3988380613a3f9f1e87f Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 1 Aug 2018 12:30:50 +0200
Subject: [PATCH 164/380] changelog: add notes about closing #12561

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index dde7ead6f13..aa089b5900b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88)
+* **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)

From af32bfebefcc02170fbaa4104ae2e5883b5c1ba8 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 14:26:29 +0200
Subject: [PATCH 165/380] Add all tests to one file

---
 .../prometheus/specs/_datasource.jest.ts      | 829 ------------------
 .../prometheus/specs/datasource.jest.ts       | 794 +++++++++++++++++
 2 files changed, 794 insertions(+), 829 deletions(-)
 delete mode 100644 public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts

diff --git a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
deleted file mode 100644
index efe2738cce9..00000000000
--- a/public/app/plugins/datasource/prometheus/specs/_datasource.jest.ts
+++ /dev/null
@@ -1,829 +0,0 @@
-import moment from 'moment';
-import { PrometheusDatasource } from '../datasource';
-import $q from 'q';
-
-const SECOND = 1000;
-const MINUTE = 60 * SECOND;
-const HOUR = 60 * MINUTE;
-
-const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND);
-
-let ctx = <any>{};
-let instanceSettings = {
-  url: 'proxied',
-  directUrl: 'direct',
-  user: 'test',
-  password: 'mupp',
-  jsonData: { httpMethod: 'GET' },
-};
-let backendSrv = <any>{
-  datasourceRequest: jest.fn(),
-};
-
-let templateSrv = {
-  replace: jest.fn(str => str),
-};
-
-let timeSrv = {
-  timeRange: () => {
-    return { to: { diff: () => 2000 }, from: '' };
-  },
-};
-
-describe('PrometheusDatasource', function() {
-  //   beforeEach(angularMocks.module('grafana.core'));
-  //   beforeEach(angularMocks.module('grafana.services'));
-  //   beforeEach(ctx.providePhase(['timeSrv']));
-
-  //   beforeEach(
-  //     angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
-  //       ctx.$q = $q;
-  //       ctx.$httpBackend = $httpBackend;
-  //       ctx.$rootScope = $rootScope;
-  //       ctx.ds = $injector.instantiate(PrometheusDatasource, {
-  //         instanceSettings: instanceSettings,
-  //       });
-  //       $httpBackend.when('GET', /\.html$/).respond('');
-  //     })
-  //   );
-
-  describe('When querying prometheus with one target using query editor target spec', async () => {
-    var results;
-    var query = {
-      range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
-      interval: '60s',
-    };
-    // Interval alignment with step
-    var urlExpected =
-      'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60';
-
-    beforeEach(async () => {
-      let response = {
-        data: {
-          status: 'success',
-          data: {
-            resultType: 'matrix',
-            result: [
-              {
-                metric: { __name__: 'test', job: 'testjob' },
-                values: [[60, '3846']],
-              },
-            ],
-          },
-        },
-      };
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-
-      await ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-    });
-
-    it('should generate the correct query', function() {
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-    it('should return series list', async () => {
-      expect(results.data.length).toBe(1);
-      expect(results.data[0].target).toBe('test{job="testjob"}');
-    });
-  });
-  describe('When querying prometheus with one target which return multiple series', function() {
-    var results;
-    var start = 60;
-    var end = 360;
-    var step = 60;
-
-    var query = {
-      range: { from: time({ seconds: start }), to: time({ seconds: end }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
-      interval: '60s',
-    };
-
-    beforeEach(async () => {
-      let response = {
-        status: 'success',
-        data: {
-          data: {
-            resultType: 'matrix',
-            result: [
-              {
-                metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
-                values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']],
-              },
-              {
-                metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
-                values: [[start + step * 2, '4846']],
-              },
-            ],
-          },
-        },
-      };
-
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-
-      await ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-    });
-
-    it('should be same length', function() {
-      expect(results.data.length).toBe(2);
-      expect(results.data[0].datapoints.length).toBe((end - start) / step + 1);
-      expect(results.data[1].datapoints.length).toBe((end - start) / step + 1);
-    });
-
-    it('should fill null until first datapoint in response', function() {
-      expect(results.data[0].datapoints[0][1]).toBe(start * 1000);
-      expect(results.data[0].datapoints[0][0]).toBe(null);
-      expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000);
-      expect(results.data[0].datapoints[1][0]).toBe(3846);
-    });
-    it('should fill null after last datapoint in response', function() {
-      var length = (end - start) / step + 1;
-      expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000);
-      expect(results.data[0].datapoints[length - 2][0]).toBe(3848);
-      expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000);
-      expect(results.data[0].datapoints[length - 1][0]).toBe(null);
-    });
-    it('should fill null at gap between series', function() {
-      expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000);
-      expect(results.data[0].datapoints[2][0]).toBe(null);
-      expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000);
-      expect(results.data[1].datapoints[1][0]).toBe(null);
-      expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000);
-      expect(results.data[1].datapoints[3][0]).toBe(null);
-    });
-  });
-  describe('When querying prometheus with one target and instant = true', function() {
-    var results;
-    var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
-    var query = {
-      range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
-      interval: '60s',
-    };
-
-    beforeEach(async () => {
-      let response = {
-        status: 'success',
-        data: {
-          data: {
-            resultType: 'vector',
-            result: [
-              {
-                metric: { __name__: 'test', job: 'testjob' },
-                value: [123, '3846'],
-              },
-            ],
-          },
-        },
-      };
-
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-
-      await ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-    });
-    it('should generate the correct query', function() {
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-    it('should return series list', function() {
-      expect(results.data.length).toBe(1);
-      expect(results.data[0].target).toBe('test{job="testjob"}');
-    });
-  });
-  describe('When performing annotationQuery', function() {
-    var results;
-
-    var options = {
-      annotation: {
-        expr: 'ALERTS{alertstate="firing"}',
-        tagKeys: 'job',
-        titleFormat: '{{alertname}}',
-        textFormat: '{{instance}}',
-      },
-      range: {
-        from: time({ seconds: 63 }),
-        to: time({ seconds: 123 }),
-      },
-    };
-
-    beforeEach(async () => {
-      let response = {
-        status: 'success',
-        data: {
-          data: {
-            resultType: 'matrix',
-            result: [
-              {
-                metric: {
-                  __name__: 'ALERTS',
-                  alertname: 'InstanceDown',
-                  alertstate: 'firing',
-                  instance: 'testinstance',
-                  job: 'testjob',
-                },
-                values: [[123, '1']],
-              },
-            ],
-          },
-        },
-      };
-
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-
-      await ctx.ds.annotationQuery(options).then(function(data) {
-        results = data;
-      });
-    });
-    it('should return annotation list', function() {
-      //   ctx.$rootScope.$apply();
-      expect(results.length).toBe(1);
-      expect(results[0].tags).toContain('testjob');
-      expect(results[0].title).toBe('InstanceDown');
-      expect(results[0].text).toBe('testinstance');
-      expect(results[0].time).toBe(123 * 1000);
-    });
-  });
-
-  describe('When resultFormat is table and instant = true', function() {
-    var results;
-    // var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
-    var query = {
-      range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
-      interval: '60s',
-    };
-
-    beforeEach(async () => {
-      let response = {
-        status: 'success',
-        data: {
-          data: {
-            resultType: 'vector',
-            result: [
-              {
-                metric: { __name__: 'test', job: 'testjob' },
-                value: [123, '3846'],
-              },
-            ],
-          },
-        },
-      };
-
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-    });
-
-    it('should return result', () => {
-      expect(results).not.toBe(null);
-    });
-  });
-
-  describe('The "step" query parameter', function() {
-    var response = {
-      status: 'success',
-      data: {
-        data: {
-          resultType: 'matrix',
-          result: [],
-        },
-      },
-    };
-
-    it('should be min interval when greater than auto interval', async () => {
-      let query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '10s',
-          },
-        ],
-        interval: '5s',
-      };
-      let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
-
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-
-    it('step should never go below 1', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [{ expr: 'test' }],
-        interval: '100ms',
-      };
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-
-    it('should be auto interval when greater than min interval', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '5s',
-          },
-        ],
-        interval: '10s',
-      };
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-    it('should result in querying fewer than 11000 data points', async () => {
-      var query = {
-        // 6 hour range
-        range: { from: time({ hours: 1 }), to: time({ hours: 7 }) },
-        targets: [{ expr: 'test' }],
-        interval: '1s',
-      };
-      var end = 7 * 60 * 60;
-      var start = 60 * 60;
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-    it('should not apply min interval when interval * intervalFactor greater', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '10s',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '5s',
-      };
-      // times get rounded up to interval
-      var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-    it('should apply min interval when interval * intervalFactor smaller', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '15s',
-            intervalFactor: 2,
-          },
-        ],
-        interval: '5s',
-      };
-      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-    it('should apply intervalFactor to auto interval when greater', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'test',
-            interval: '5s',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '10s',
-      };
-      // times get aligned to interval
-      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-    it('should not not be affected by the 11000 data points limit when large enough', async () => {
-      var query = {
-        // 1 week range
-        range: { from: time({}), to: time({ hours: 7 * 24 }) },
-        targets: [
-          {
-            expr: 'test',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '10s',
-      };
-      var end = 7 * 24 * 60 * 60;
-      var start = 0;
-      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-    it('should be determined by the 11000 data points limit when too small', async () => {
-      var query = {
-        // 1 week range
-        range: { from: time({}), to: time({ hours: 7 * 24 }) },
-        targets: [
-          {
-            expr: 'test',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '5s',
-      };
-      var end = 7 * 24 * 60 * 60;
-      var start = 0;
-      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-    });
-  });
-
-  describe('The __interval and __interval_ms template variables', function() {
-    var response = {
-      status: 'success',
-      data: {
-        data: {
-          resultType: 'matrix',
-          result: [],
-        },
-      },
-    };
-
-    it('should be unchanged when auto interval is greater than min interval', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '5s',
-          },
-        ],
-        interval: '10s',
-        scopedVars: {
-          __interval: { text: '10s', value: '10s' },
-          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
-        },
-      };
-
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' +
-        encodeURIComponent('rate(test[$__interval])') +
-        '&start=60&end=420&step=10';
-
-      templateSrv.replace = jest.fn(str => str);
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-
-      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
-        __interval: {
-          text: '10s',
-          value: '10s',
-        },
-        __interval_ms: {
-          text: 10000,
-          value: 10000,
-        },
-      });
-    });
-    it('should be min interval when it is greater than auto interval', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '10s',
-          },
-        ],
-        interval: '5s',
-        scopedVars: {
-          __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' +
-        encodeURIComponent('rate(test[$__interval])') +
-        '&start=60&end=420&step=10';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      templateSrv.replace = jest.fn(str => str);
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-
-      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
-        __interval: {
-          text: '5s',
-          value: '5s',
-        },
-        __interval_ms: {
-          text: 5000,
-          value: 5000,
-        },
-      });
-    });
-    it('should account for intervalFactor', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '5s',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '10s',
-        scopedVars: {
-          __interval: { text: '10s', value: '10s' },
-          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' +
-        encodeURIComponent('rate(test[$__interval])') +
-        '&start=0&end=500&step=100';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      templateSrv.replace = jest.fn(str => str);
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-
-      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
-        __interval: {
-          text: '10s',
-          value: '10s',
-        },
-        __interval_ms: {
-          text: 10000,
-          value: 10000,
-        },
-      });
-
-      expect(query.scopedVars.__interval.text).toBe('10s');
-      expect(query.scopedVars.__interval.value).toBe('10s');
-      expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
-      expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000);
-    });
-    it('should be interval * intervalFactor when greater than min interval', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '10s',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '5s',
-        scopedVars: {
-          __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' +
-        encodeURIComponent('rate(test[$__interval])') +
-        '&start=50&end=450&step=50';
-
-      templateSrv.replace = jest.fn(str => str);
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-
-      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
-        __interval: {
-          text: '5s',
-          value: '5s',
-        },
-        __interval_ms: {
-          text: 5000,
-          value: 5000,
-        },
-      });
-    });
-    it('should be min interval when greater than interval * intervalFactor', async () => {
-      var query = {
-        // 6 minute range
-        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            interval: '15s',
-            intervalFactor: 2,
-          },
-        ],
-        interval: '5s',
-        scopedVars: {
-          __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
-        },
-      };
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' +
-        encodeURIComponent('rate(test[$__interval])') +
-        '&start=60&end=420&step=15';
-
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-
-      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
-        __interval: {
-          text: '5s',
-          value: '5s',
-        },
-        __interval_ms: {
-          text: 5000,
-          value: 5000,
-        },
-      });
-    });
-    it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => {
-      var query = {
-        // 1 week range
-        range: { from: time({}), to: time({ hours: 7 * 24 }) },
-        targets: [
-          {
-            expr: 'rate(test[$__interval])',
-            intervalFactor: 10,
-          },
-        ],
-        interval: '5s',
-        scopedVars: {
-          __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
-        },
-      };
-      var end = 7 * 24 * 60 * 60;
-      var start = 0;
-      var urlExpected =
-        'proxied/api/v1/query_range?query=' +
-        encodeURIComponent('rate(test[$__interval])') +
-        '&start=' +
-        start +
-        '&end=' +
-        end +
-        '&step=60';
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      templateSrv.replace = jest.fn(str => str);
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query);
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('GET');
-      expect(res.url).toBe(urlExpected);
-
-      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
-        __interval: {
-          text: '5s',
-          value: '5s',
-        },
-        __interval_ms: {
-          text: 5000,
-          value: 5000,
-        },
-      });
-    });
-  });
-});
-
-describe('PrometheusDatasource for POST', function() {
-  //   var ctx = new helpers.ServiceTestContext();
-  let instanceSettings = {
-    url: 'proxied',
-    directUrl: 'direct',
-    user: 'test',
-    password: 'mupp',
-    jsonData: { httpMethod: 'POST' },
-  };
-
-  //   beforeEach(angularMocks.module('grafana.core'));
-  //   beforeEach(angularMocks.module('grafana.services'));
-  //   beforeEach(ctx.providePhase(['timeSrv']));
-
-  //   beforeEach(
-  //     // angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
-  //     //   ctx.$q = $q;
-  //     //   ctx.$httpBackend = $httpBackend;
-  //     //   ctx.$rootScope = $rootScope;
-  //     //   ctx.ds = $injector.instantiate(PrometheusDatasource, { instanceSettings: instanceSettings });
-  //     //   $httpBackend.when('GET', /\.html$/).respond('');
-  //     // })
-  //   );
-
-  describe('When querying prometheus with one target using query editor target spec', function() {
-    var results;
-    var urlExpected = 'proxied/api/v1/query_range';
-    var dataExpected = {
-      query: 'test{job="testjob"}',
-      start: 1 * 60,
-      end: 3 * 60,
-      step: 60,
-    };
-    var query = {
-      range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) },
-      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
-      interval: '60s',
-    };
-
-    beforeEach(async () => {
-      let response = {
-        status: 'success',
-        data: {
-          data: {
-            resultType: 'matrix',
-            result: [
-              {
-                metric: { __name__: 'test', job: 'testjob' },
-                values: [[2 * 60, '3846']],
-              },
-            ],
-          },
-        },
-      };
-      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, $q, <any>backendSrv, templateSrv, timeSrv);
-      await ctx.ds.query(query).then(function(data) {
-        results = data;
-      });
-    });
-    it('should generate the correct query', function() {
-      let res = backendSrv.datasourceRequest.mock.calls[0][0];
-      expect(res.method).toBe('POST');
-      expect(res.url).toBe(urlExpected);
-      expect(res.data).toEqual(dataExpected);
-    });
-    it('should return series list', function() {
-      expect(results.data.length).toBe(1);
-      expect(results.data[0].target).toBe('test{job="testjob"}');
-    });
-  });
-});
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index b8b2b50f590..f60af583f45 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -246,3 +246,797 @@ describe('PrometheusDatasource', () => {
     });
   });
 });
+
+const SECOND = 1000;
+const MINUTE = 60 * SECOND;
+const HOUR = 60 * MINUTE;
+
+const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND);
+
+let ctx = <any>{};
+let instanceSettings = {
+  url: 'proxied',
+  directUrl: 'direct',
+  user: 'test',
+  password: 'mupp',
+  jsonData: { httpMethod: 'GET' },
+};
+let backendSrv = <any>{
+  datasourceRequest: jest.fn(),
+};
+
+let templateSrv = {
+  replace: jest.fn(str => str),
+};
+
+let timeSrv = {
+  timeRange: () => {
+    return { to: { diff: () => 2000 }, from: '' };
+  },
+};
+
+describe('PrometheusDatasource', function() {
+  describe('When querying prometheus with one target using query editor target spec', async () => {
+    var results;
+    var query = {
+      range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
+      interval: '60s',
+    };
+    // Interval alignment with step
+    var urlExpected =
+      'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60';
+
+    beforeEach(async () => {
+      let response = {
+        data: {
+          status: 'success',
+          data: {
+            resultType: 'matrix',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob' },
+                values: [[60, '3846']],
+              },
+            ],
+          },
+        },
+      };
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+
+    it('should generate the correct query', function() {
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should return series list', async () => {
+      expect(results.data.length).toBe(1);
+      expect(results.data[0].target).toBe('test{job="testjob"}');
+    });
+  });
+  describe('When querying prometheus with one target which return multiple series', function() {
+    var results;
+    var start = 60;
+    var end = 360;
+    var step = 60;
+
+    var query = {
+      range: { from: time({ seconds: start }), to: time({ seconds: end }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
+      interval: '60s',
+    };
+
+    beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'matrix',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob', series: 'series 1' },
+                values: [[start + step * 1, '3846'], [start + step * 3, '3847'], [end - step * 1, '3848']],
+              },
+              {
+                metric: { __name__: 'test', job: 'testjob', series: 'series 2' },
+                values: [[start + step * 2, '4846']],
+              },
+            ],
+          },
+        },
+      };
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+
+    it('should be same length', function() {
+      expect(results.data.length).toBe(2);
+      expect(results.data[0].datapoints.length).toBe((end - start) / step + 1);
+      expect(results.data[1].datapoints.length).toBe((end - start) / step + 1);
+    });
+
+    it('should fill null until first datapoint in response', function() {
+      expect(results.data[0].datapoints[0][1]).toBe(start * 1000);
+      expect(results.data[0].datapoints[0][0]).toBe(null);
+      expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000);
+      expect(results.data[0].datapoints[1][0]).toBe(3846);
+    });
+    it('should fill null after last datapoint in response', function() {
+      var length = (end - start) / step + 1;
+      expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000);
+      expect(results.data[0].datapoints[length - 2][0]).toBe(3848);
+      expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000);
+      expect(results.data[0].datapoints[length - 1][0]).toBe(null);
+    });
+    it('should fill null at gap between series', function() {
+      expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000);
+      expect(results.data[0].datapoints[2][0]).toBe(null);
+      expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000);
+      expect(results.data[1].datapoints[1][0]).toBe(null);
+      expect(results.data[1].datapoints[3][1]).toBe((start + step * 3) * 1000);
+      expect(results.data[1].datapoints[3][0]).toBe(null);
+    });
+  });
+  describe('When querying prometheus with one target and instant = true', function() {
+    var results;
+    var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
+    var query = {
+      range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
+      interval: '60s',
+    };
+
+    beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'vector',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob' },
+                value: [123, '3846'],
+              },
+            ],
+          },
+        },
+      };
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+    it('should generate the correct query', function() {
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should return series list', function() {
+      expect(results.data.length).toBe(1);
+      expect(results.data[0].target).toBe('test{job="testjob"}');
+    });
+  });
+  describe('When performing annotationQuery', function() {
+    var results;
+
+    var options = {
+      annotation: {
+        expr: 'ALERTS{alertstate="firing"}',
+        tagKeys: 'job',
+        titleFormat: '{{alertname}}',
+        textFormat: '{{instance}}',
+      },
+      range: {
+        from: time({ seconds: 63 }),
+        to: time({ seconds: 123 }),
+      },
+    };
+
+    beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'matrix',
+            result: [
+              {
+                metric: {
+                  __name__: 'ALERTS',
+                  alertname: 'InstanceDown',
+                  alertstate: 'firing',
+                  instance: 'testinstance',
+                  job: 'testjob',
+                },
+                values: [[123, '1']],
+              },
+            ],
+          },
+        },
+      };
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+
+      await ctx.ds.annotationQuery(options).then(function(data) {
+        results = data;
+      });
+    });
+    it('should return annotation list', function() {
+      expect(results.length).toBe(1);
+      expect(results[0].tags).toContain('testjob');
+      expect(results[0].title).toBe('InstanceDown');
+      expect(results[0].text).toBe('testinstance');
+      expect(results[0].time).toBe(123 * 1000);
+    });
+  });
+
+  describe('When resultFormat is table and instant = true', function() {
+    var results;
+    var query = {
+      range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
+      interval: '60s',
+    };
+
+    beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'vector',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob' },
+                value: [123, '3846'],
+              },
+            ],
+          },
+        },
+      };
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+
+    it('should return result', () => {
+      expect(results).not.toBe(null);
+    });
+  });
+
+  describe('The "step" query parameter', function() {
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'matrix',
+          result: [],
+        },
+      },
+    };
+
+    it('should be min interval when greater than auto interval', async () => {
+      let query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '10s',
+          },
+        ],
+        interval: '5s',
+      };
+      let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+
+    it('step should never go below 1', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [{ expr: 'test' }],
+        interval: '100ms',
+      };
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+
+    it('should be auto interval when greater than min interval', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '5s',
+          },
+        ],
+        interval: '10s',
+      };
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should result in querying fewer than 11000 data points', async () => {
+      var query = {
+        // 6 hour range
+        range: { from: time({ hours: 1 }), to: time({ hours: 7 }) },
+        targets: [{ expr: 'test' }],
+        interval: '1s',
+      };
+      var end = 7 * 60 * 60;
+      var start = 60 * 60;
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should not apply min interval when interval * intervalFactor greater', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '10s',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '5s',
+      };
+      // times get rounded up to interval
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should apply min interval when interval * intervalFactor smaller', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '15s',
+            intervalFactor: 2,
+          },
+        ],
+        interval: '5s',
+      };
+      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should apply intervalFactor to auto interval when greater', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'test',
+            interval: '5s',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '10s',
+      };
+      // times get aligned to interval
+      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should not not be affected by the 11000 data points limit when large enough', async () => {
+      var query = {
+        // 1 week range
+        range: { from: time({}), to: time({ hours: 7 * 24 }) },
+        targets: [
+          {
+            expr: 'test',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '10s',
+      };
+      var end = 7 * 24 * 60 * 60;
+      var start = 0;
+      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+    it('should be determined by the 11000 data points limit when too small', async () => {
+      var query = {
+        // 1 week range
+        range: { from: time({}), to: time({ hours: 7 * 24 }) },
+        targets: [
+          {
+            expr: 'test',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '5s',
+      };
+      var end = 7 * 24 * 60 * 60;
+      var start = 0;
+      var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+    });
+  });
+
+  describe('The __interval and __interval_ms template variables', function() {
+    var response = {
+      status: 'success',
+      data: {
+        data: {
+          resultType: 'matrix',
+          result: [],
+        },
+      },
+    };
+
+    it('should be unchanged when auto interval is greater than min interval', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '5s',
+          },
+        ],
+        interval: '10s',
+        scopedVars: {
+          __interval: { text: '10s', value: '10s' },
+          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
+        },
+      };
+
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=60&end=420&step=10';
+
+      templateSrv.replace = jest.fn(str => str);
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '10s',
+          value: '10s',
+        },
+        __interval_ms: {
+          text: 10000,
+          value: 10000,
+        },
+      });
+    });
+    it('should be min interval when it is greater than auto interval', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '10s',
+          },
+        ],
+        interval: '5s',
+        scopedVars: {
+          __interval: { text: '5s', value: '5s' },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=60&end=420&step=10';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      templateSrv.replace = jest.fn(str => str);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '5s',
+          value: '5s',
+        },
+        __interval_ms: {
+          text: 5000,
+          value: 5000,
+        },
+      });
+    });
+    it('should account for intervalFactor', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '5s',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '10s',
+        scopedVars: {
+          __interval: { text: '10s', value: '10s' },
+          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=0&end=500&step=100';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      templateSrv.replace = jest.fn(str => str);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '10s',
+          value: '10s',
+        },
+        __interval_ms: {
+          text: 10000,
+          value: 10000,
+        },
+      });
+
+      expect(query.scopedVars.__interval.text).toBe('10s');
+      expect(query.scopedVars.__interval.value).toBe('10s');
+      expect(query.scopedVars.__interval_ms.text).toBe(10 * 1000);
+      expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000);
+    });
+    it('should be interval * intervalFactor when greater than min interval', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '10s',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '5s',
+        scopedVars: {
+          __interval: { text: '5s', value: '5s' },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=50&end=450&step=50';
+
+      templateSrv.replace = jest.fn(str => str);
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '5s',
+          value: '5s',
+        },
+        __interval_ms: {
+          text: 5000,
+          value: 5000,
+        },
+      });
+    });
+    it('should be min interval when greater than interval * intervalFactor', async () => {
+      var query = {
+        // 6 minute range
+        range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            interval: '15s',
+            intervalFactor: 2,
+          },
+        ],
+        interval: '5s',
+        scopedVars: {
+          __interval: { text: '5s', value: '5s' },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
+        },
+      };
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=60&end=420&step=15';
+
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '5s',
+          value: '5s',
+        },
+        __interval_ms: {
+          text: 5000,
+          value: 5000,
+        },
+      });
+    });
+    it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => {
+      var query = {
+        // 1 week range
+        range: { from: time({}), to: time({ hours: 7 * 24 }) },
+        targets: [
+          {
+            expr: 'rate(test[$__interval])',
+            intervalFactor: 10,
+          },
+        ],
+        interval: '5s',
+        scopedVars: {
+          __interval: { text: '5s', value: '5s' },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
+        },
+      };
+      var end = 7 * 24 * 60 * 60;
+      var start = 0;
+      var urlExpected =
+        'proxied/api/v1/query_range?query=' +
+        encodeURIComponent('rate(test[$__interval])') +
+        '&start=' +
+        start +
+        '&end=' +
+        end +
+        '&step=60';
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      templateSrv.replace = jest.fn(str => str);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query);
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('GET');
+      expect(res.url).toBe(urlExpected);
+
+      expect(templateSrv.replace.mock.calls[0][1]).toEqual({
+        __interval: {
+          text: '5s',
+          value: '5s',
+        },
+        __interval_ms: {
+          text: 5000,
+          value: 5000,
+        },
+      });
+    });
+  });
+});
+
+describe('PrometheusDatasource for POST', function() {
+  //   var ctx = new helpers.ServiceTestContext();
+  let instanceSettings = {
+    url: 'proxied',
+    directUrl: 'direct',
+    user: 'test',
+    password: 'mupp',
+    jsonData: { httpMethod: 'POST' },
+  };
+
+  describe('When querying prometheus with one target using query editor target spec', function() {
+    var results;
+    var urlExpected = 'proxied/api/v1/query_range';
+    var dataExpected = {
+      query: 'test{job="testjob"}',
+      start: 1 * 60,
+      end: 3 * 60,
+      step: 60,
+    };
+    var query = {
+      range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) },
+      targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
+      interval: '60s',
+    };
+
+    beforeEach(async () => {
+      let response = {
+        status: 'success',
+        data: {
+          data: {
+            resultType: 'matrix',
+            result: [
+              {
+                metric: { __name__: 'test', job: 'testjob' },
+                values: [[2 * 60, '3846']],
+              },
+            ],
+          },
+        },
+      };
+      backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, <any>backendSrv, templateSrv, timeSrv);
+      await ctx.ds.query(query).then(function(data) {
+        results = data;
+      });
+    });
+    it('should generate the correct query', function() {
+      let res = backendSrv.datasourceRequest.mock.calls[0][0];
+      expect(res.method).toBe('POST');
+      expect(res.url).toBe(urlExpected);
+      expect(res.data).toEqual(dataExpected);
+    });
+    it('should return series list', function() {
+      expect(results.data.length).toBe(1);
+      expect(results.data[0].target).toBe('test{job="testjob"}');
+    });
+  });
+});

From 951b623bd23ca1aa43833e2898876579c8417370 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 14:27:45 +0200
Subject: [PATCH 166/380] Change to arrow functions

---
 .../prometheus/specs/datasource.jest.ts       | 66 +++++++++----------
 1 file changed, 33 insertions(+), 33 deletions(-)

diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index f60af583f45..aeca8d69191 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -150,49 +150,49 @@ describe('PrometheusDatasource', () => {
     });
   });
 
-  describe('alignRange', function() {
-    it('does not modify already aligned intervals with perfect step', function() {
+  describe('alignRange', () => {
+    it('does not modify already aligned intervals with perfect step', () => {
       const range = alignRange(0, 3, 3);
       expect(range.start).toEqual(0);
       expect(range.end).toEqual(3);
     });
-    it('does modify end-aligned intervals to reflect number of steps possible', function() {
+    it('does modify end-aligned intervals to reflect number of steps possible', () => {
       const range = alignRange(1, 6, 3);
       expect(range.start).toEqual(0);
       expect(range.end).toEqual(6);
     });
-    it('does align intervals that are a multiple of steps', function() {
+    it('does align intervals that are a multiple of steps', () => {
       const range = alignRange(1, 4, 3);
       expect(range.start).toEqual(0);
       expect(range.end).toEqual(6);
     });
-    it('does align intervals that are not a multiple of steps', function() {
+    it('does align intervals that are not a multiple of steps', () => {
       const range = alignRange(1, 5, 3);
       expect(range.start).toEqual(0);
       expect(range.end).toEqual(6);
     });
   });
 
-  describe('Prometheus regular escaping', function() {
-    it('should not escape non-string', function() {
+  describe('Prometheus regular escaping', () => {
+    it('should not escape non-string', () => {
       expect(prometheusRegularEscape(12)).toEqual(12);
     });
-    it('should not escape simple string', function() {
+    it('should not escape simple string', () => {
       expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
     });
-    it("should escape '", function() {
+    it("should escape '", () => {
       expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
     });
-    it('should escape multiple characters', function() {
+    it('should escape multiple characters', () => {
       expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
     });
   });
 
-  describe('Prometheus regexes escaping', function() {
-    it('should not escape simple string', function() {
+  describe('Prometheus regexes escaping', () => {
+    it('should not escape simple string', () => {
       expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
     });
-    it('should escape $^*+?.()\\', function() {
+    it('should escape $^*+?.()\\', () => {
       expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass");
       expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass');
       expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass');
@@ -208,7 +208,7 @@ describe('PrometheusDatasource', () => {
       expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass');
       expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass');
     });
-    it('should escape multiple special characters', function() {
+    it('should escape multiple special characters', () => {
       expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
     });
   });
@@ -275,7 +275,7 @@ let timeSrv = {
   },
 };
 
-describe('PrometheusDatasource', function() {
+describe('PrometheusDatasource', () => {
   describe('When querying prometheus with one target using query editor target spec', async () => {
     var results;
     var query = {
@@ -310,7 +310,7 @@ describe('PrometheusDatasource', function() {
       });
     });
 
-    it('should generate the correct query', function() {
+    it('should generate the correct query', () => {
       let res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
@@ -320,7 +320,7 @@ describe('PrometheusDatasource', function() {
       expect(results.data[0].target).toBe('test{job="testjob"}');
     });
   });
-  describe('When querying prometheus with one target which return multiple series', function() {
+  describe('When querying prometheus with one target which return multiple series', () => {
     var results;
     var start = 60;
     var end = 360;
@@ -360,26 +360,26 @@ describe('PrometheusDatasource', function() {
       });
     });
 
-    it('should be same length', function() {
+    it('should be same length', () => {
       expect(results.data.length).toBe(2);
       expect(results.data[0].datapoints.length).toBe((end - start) / step + 1);
       expect(results.data[1].datapoints.length).toBe((end - start) / step + 1);
     });
 
-    it('should fill null until first datapoint in response', function() {
+    it('should fill null until first datapoint in response', () => {
       expect(results.data[0].datapoints[0][1]).toBe(start * 1000);
       expect(results.data[0].datapoints[0][0]).toBe(null);
       expect(results.data[0].datapoints[1][1]).toBe((start + step * 1) * 1000);
       expect(results.data[0].datapoints[1][0]).toBe(3846);
     });
-    it('should fill null after last datapoint in response', function() {
+    it('should fill null after last datapoint in response', () => {
       var length = (end - start) / step + 1;
       expect(results.data[0].datapoints[length - 2][1]).toBe((end - step * 1) * 1000);
       expect(results.data[0].datapoints[length - 2][0]).toBe(3848);
       expect(results.data[0].datapoints[length - 1][1]).toBe(end * 1000);
       expect(results.data[0].datapoints[length - 1][0]).toBe(null);
     });
-    it('should fill null at gap between series', function() {
+    it('should fill null at gap between series', () => {
       expect(results.data[0].datapoints[2][1]).toBe((start + step * 2) * 1000);
       expect(results.data[0].datapoints[2][0]).toBe(null);
       expect(results.data[1].datapoints[1][1]).toBe((start + step * 1) * 1000);
@@ -388,7 +388,7 @@ describe('PrometheusDatasource', function() {
       expect(results.data[1].datapoints[3][0]).toBe(null);
     });
   });
-  describe('When querying prometheus with one target and instant = true', function() {
+  describe('When querying prometheus with one target and instant = true', () => {
     var results;
     var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
     var query = {
@@ -420,17 +420,17 @@ describe('PrometheusDatasource', function() {
         results = data;
       });
     });
-    it('should generate the correct query', function() {
+    it('should generate the correct query', () => {
       let res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
       expect(res.url).toBe(urlExpected);
     });
-    it('should return series list', function() {
+    it('should return series list', () => {
       expect(results.data.length).toBe(1);
       expect(results.data[0].target).toBe('test{job="testjob"}');
     });
   });
-  describe('When performing annotationQuery', function() {
+  describe('When performing annotationQuery', () => {
     var results;
 
     var options = {
@@ -475,7 +475,7 @@ describe('PrometheusDatasource', function() {
         results = data;
       });
     });
-    it('should return annotation list', function() {
+    it('should return annotation list', () => {
       expect(results.length).toBe(1);
       expect(results[0].tags).toContain('testjob');
       expect(results[0].title).toBe('InstanceDown');
@@ -484,7 +484,7 @@ describe('PrometheusDatasource', function() {
     });
   });
 
-  describe('When resultFormat is table and instant = true', function() {
+  describe('When resultFormat is table and instant = true', () => {
     var results;
     var query = {
       range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
@@ -520,7 +520,7 @@ describe('PrometheusDatasource', function() {
     });
   });
 
-  describe('The "step" query parameter', function() {
+  describe('The "step" query parameter', () => {
     var response = {
       status: 'success',
       data: {
@@ -717,7 +717,7 @@ describe('PrometheusDatasource', function() {
     });
   });
 
-  describe('The __interval and __interval_ms template variables', function() {
+  describe('The __interval and __interval_ms template variables', () => {
     var response = {
       status: 'success',
       data: {
@@ -982,7 +982,7 @@ describe('PrometheusDatasource', function() {
   });
 });
 
-describe('PrometheusDatasource for POST', function() {
+describe('PrometheusDatasource for POST', () => {
   //   var ctx = new helpers.ServiceTestContext();
   let instanceSettings = {
     url: 'proxied',
@@ -992,7 +992,7 @@ describe('PrometheusDatasource for POST', function() {
     jsonData: { httpMethod: 'POST' },
   };
 
-  describe('When querying prometheus with one target using query editor target spec', function() {
+  describe('When querying prometheus with one target using query editor target spec', () => {
     var results;
     var urlExpected = 'proxied/api/v1/query_range';
     var dataExpected = {
@@ -1028,13 +1028,13 @@ describe('PrometheusDatasource for POST', function() {
         results = data;
       });
     });
-    it('should generate the correct query', function() {
+    it('should generate the correct query', () => {
       let res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('POST');
       expect(res.url).toBe(urlExpected);
       expect(res.data).toEqual(dataExpected);
     });
-    it('should return series list', function() {
+    it('should return series list', () => {
       expect(results.data.length).toBe(1);
       expect(results.data[0].target).toBe('test{job="testjob"}');
     });

From dc22e24642f79b1130de2fc4f15d911c2973f5d0 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Wed, 1 Aug 2018 15:06:18 +0200
Subject: [PATCH 167/380] add compatibility code to handle pre 5.3 usage

---
 pkg/tsdb/postgres/macros.go      | 17 +++++++++++++++++
 pkg/tsdb/postgres/macros_test.go | 19 ++++++++++++++++---
 2 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index fa887032c5d..9e337caf3ec 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -30,6 +30,23 @@ func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.Tim
 	var macroError error
 
 	sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
+
+		// detect if $__timeGroup is supposed to add AS time for pre 5.3 compatibility
+		// if there is a ',' directly after the macro call $__timeGroup is probably used
+		// in the old way. Inside window function ORDER BY $__timeGroup will be followed
+		// by ')'
+		if groups[1] == "__timeGroup" {
+			if index := strings.Index(sql, groups[0]); index >= 0 {
+				index += len(groups[0])
+				if len(sql) > index {
+					// check for character after macro expression
+					if sql[index] == ',' {
+						groups[1] = "__timeGroupAlias"
+					}
+				}
+			}
+		}
+
 		args := strings.Split(groups[2], ",")
 		for i, arg := range args {
 			args[i] = strings.Trim(arg, " ")
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index ec74470a803..beeea93893b 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -48,14 +48,27 @@ func TestMacroEngine(t *testing.T) {
 				So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
 			})
 
+			Convey("interpolate __timeGroup function pre 5.3 compatibility", func() {
+
+				sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m'), value")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 AS \"time\", value")
+
+				sql, err = engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m') as time, value")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 as time, value")
+			})
+
 			Convey("interpolate __timeGroup function", func() {
 
-				sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column,'5m')")
+				sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
-				sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column,'5m')")
+				sql2, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroupAlias(time_column,'5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300")
+				So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300")
 				So(sql2, ShouldEqual, sql+" AS \"time\"")
 			})
 

From bb7e5838635fa044e75507c03827e4ba97cb7f53 Mon Sep 17 00:00:00 2001
From: Brice Maron <brice@bmaron.net>
Date: Wed, 1 Aug 2018 19:38:13 +0200
Subject: [PATCH 168/380] fix custom variable quoting in sql* query
 interpolations

---
 public/app/plugins/datasource/mssql/datasource.ts          | 4 ++--
 .../app/plugins/datasource/mssql/specs/datasource.jest.ts  | 7 +++++++
 public/app/plugins/datasource/mysql/datasource.ts          | 4 ++--
 .../app/plugins/datasource/mysql/specs/datasource.jest.ts  | 7 +++++++
 public/app/plugins/datasource/postgres/datasource.ts       | 4 ++--
 .../plugins/datasource/postgres/specs/datasource.jest.ts   | 7 +++++++
 6 files changed, 27 insertions(+), 6 deletions(-)

diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts
index 6656d4f96f7..dab7335ec97 100644
--- a/public/app/plugins/datasource/mssql/datasource.ts
+++ b/public/app/plugins/datasource/mssql/datasource.ts
@@ -16,7 +16,7 @@ export class MssqlDatasource {
   interpolateVariable(value, variable) {
     if (typeof value === 'string') {
       if (variable.multi || variable.includeAll) {
-        return "'" + value + "'";
+        return "'" + value.replace(/'/g, `''`) + "'";
       } else {
         return value;
       }
@@ -31,7 +31,7 @@ export class MssqlDatasource {
         return value;
       }
 
-      return "'" + val + "'";
+      return "'" + val.replace(/'/g, `''`) + "'";
     });
     return quotedValues.join(',');
   }
diff --git a/public/app/plugins/datasource/mssql/specs/datasource.jest.ts b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts
index dd2d4a60cec..0308717775b 100644
--- a/public/app/plugins/datasource/mssql/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts
@@ -218,6 +218,13 @@ describe('MSSQLDatasource', function() {
       });
     });
 
+    describe('and variable contains single quote', () => {
+      it('should return a quoted value', () => {
+        ctx.variable.multi = true;
+        expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'");
+      });
+    });
+
     describe('and variable allows all and value is a string', () => {
       it('should return a quoted value', () => {
         ctx.variable.includeAll = true;
diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts
index 42fcf7b4564..67bb9d0a817 100644
--- a/public/app/plugins/datasource/mysql/datasource.ts
+++ b/public/app/plugins/datasource/mysql/datasource.ts
@@ -16,7 +16,7 @@ export class MysqlDatasource {
   interpolateVariable(value, variable) {
     if (typeof value === 'string') {
       if (variable.multi || variable.includeAll) {
-        return "'" + value + "'";
+        return "'" + value.replace(/'/g, `''`) + "'";
       } else {
         return value;
       }
@@ -31,7 +31,7 @@ export class MysqlDatasource {
         return value;
       }
 
-      return "'" + val + "'";
+      return "'" + val.replace(/'/g, `''`) + "'";
     });
     return quotedValues.join(',');
   }
diff --git a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts
index be33f5f8858..85fa2b8cc4e 100644
--- a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts
@@ -214,6 +214,13 @@ describe('MySQLDatasource', function() {
       });
     });
 
+    describe('and variable contains single quote', () => {
+      it('should return a quoted value', () => {
+        ctx.variable.multi = true;
+        expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'");
+      });
+    });
+
     describe('and variable allows all and value is a string', () => {
       it('should return a quoted value', () => {
         ctx.variable.includeAll = true;
diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts
index 8eee389d1a5..644c9e48b9b 100644
--- a/public/app/plugins/datasource/postgres/datasource.ts
+++ b/public/app/plugins/datasource/postgres/datasource.ts
@@ -16,7 +16,7 @@ export class PostgresDatasource {
   interpolateVariable(value, variable) {
     if (typeof value === 'string') {
       if (variable.multi || variable.includeAll) {
-        return "'" + value + "'";
+        return "'" + value.replace(/'/g, `''`) + "'";
       } else {
         return value;
       }
@@ -27,7 +27,7 @@ export class PostgresDatasource {
     }
 
     var quotedValues = _.map(value, function(val) {
-      return "'" + val + "'";
+      return "'" + val.replace(/'/g, `''`) + "'";
     });
     return quotedValues.join(',');
   }
diff --git a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts
index 107cd76e6c5..cd6f57ee3fc 100644
--- a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts
@@ -215,6 +215,13 @@ describe('PostgreSQLDatasource', function() {
       });
     });
 
+    describe('and variable contains single quote', () => {
+      it('should return a quoted value', () => {
+        ctx.variable.multi = true;
+        expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a''bc'");
+      });
+    });
+
     describe('and variable allows all and is a string', () => {
       it('should return a quoted value', () => {
         ctx.variable.includeAll = true;

From b71d10a7a42d9b47b191e981576cb17363f11a9d Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Wed, 1 Aug 2018 20:58:51 +0200
Subject: [PATCH 169/380] add $__timeGroupAlias to mysql and mssql

---
 pkg/tsdb/mssql/macros.go      | 6 ++++++
 pkg/tsdb/mssql/macros_test.go | 6 ++++++
 pkg/tsdb/mysql/macros.go      | 6 ++++++
 pkg/tsdb/mysql/macros_test.go | 6 ++++++
 4 files changed, 24 insertions(+)

diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index 2c16b5cb27f..f33ab1d40be 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -110,6 +110,12 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			}
 		}
 		return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__timeGroupAlias":
+		tg, err := m.evaluateMacro("__timeGroup", args)
+		if err == nil {
+			return tg + " AS [time]", err
+		}
+		return "", err
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go
index 1895cd99442..ea50c418de7 100644
--- a/pkg/tsdb/mssql/macros_test.go
+++ b/pkg/tsdb/mssql/macros_test.go
@@ -55,15 +55,21 @@ func TestMacroEngine(t *testing.T) {
 			Convey("interpolate __timeGroup function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')")
+				So(err, ShouldBeNil)
 
 				So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
+				So(sql2, ShouldEqual, sql+" AS [time]")
 			})
 
 			Convey("interpolate __timeGroup function with spaces around arguments", func() {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')")
+				So(err, ShouldBeNil)
 
 				So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
+				So(sql2, ShouldEqual, sql+" AS [time]")
 			})
 
 			Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index 078d1ff54f8..a56fd1ceb2a 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -105,6 +105,12 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			}
 		}
 		return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__timeGroupAlias":
+		tg, err := m.evaluateMacro("__timeGroup", args)
+		if err == nil {
+			return tg + " AS \"time\"", err
+		}
+		return "", err
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go
index 003af9a737f..fd9d3f5688a 100644
--- a/pkg/tsdb/mysql/macros_test.go
+++ b/pkg/tsdb/mysql/macros_test.go
@@ -38,16 +38,22 @@ func TestMacroEngine(t *testing.T) {
 
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')")
+				So(err, ShouldBeNil)
 
 				So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
+				So(sql2, ShouldEqual, sql+" AS \"time\"")
 			})
 
 			Convey("interpolate __timeGroup function with spaces around arguments", func() {
 
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')")
+				So(err, ShouldBeNil)
 
 				So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
+				So(sql2, ShouldEqual, sql+" AS \"time\"")
 			})
 
 			Convey("interpolate __timeFilter function", func() {

From 82c473e3af4800a8cb9f20c96530190b6c44d847 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Wed, 1 Aug 2018 21:23:00 +0200
Subject: [PATCH 170/380] document $__timeGroupAlias

---
 docs/sources/features/datasources/mssql.md                       | 1 +
 docs/sources/features/datasources/mysql.md                       | 1 +
 docs/sources/features/datasources/postgres.md                    | 1 +
 public/app/plugins/datasource/mssql/partials/query.editor.html   | 1 +
 public/app/plugins/datasource/mysql/partials/query.editor.html   | 1 +
 .../app/plugins/datasource/postgres/partials/query.editor.html   | 1 +
 6 files changed, 6 insertions(+)

diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
index ea7be8e1c30..dabb896ec0f 100644
--- a/docs/sources/features/datasources/mssql.md
+++ b/docs/sources/features/datasources/mssql.md
@@ -82,6 +82,7 @@ Macro example | Description
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value. <br/>For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*.
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index 22287b2a838..a0e67037005 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -65,6 +65,7 @@ Macro example | Description
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
 *$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index 7915f29fcdc..35dfcac15c0 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -62,6 +62,7 @@ Macro example | Description
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300*
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html
index 397a35164c0..e1320aabde2 100644
--- a/public/app/plugins/datasource/mssql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mssql/partials/query.editor.html
@@ -54,6 +54,7 @@ Macros:
 - $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
 - $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
 - $__timeGroup(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.
+- $__timeGroupAlias(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time]
 
 Example of group by and order by with $__timeGroup:
 SELECT
diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html
index d4be22fc3e9..db12a3fe8ce 100644
--- a/public/app/plugins/datasource/mysql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mysql/partials/query.editor.html
@@ -54,6 +54,7 @@ Macros:
 - $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
 - $__unixEpochFilter(column) -&gt;  time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
 - $__timeGroup(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
+- $__timeGroupAlias(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time"
 
 Example of group by and order by with $__timeGroup:
 SELECT
diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html
index 1ace05abae2..1b7278f6809 100644
--- a/public/app/plugins/datasource/postgres/partials/query.editor.html
+++ b/public/app/plugins/datasource/postgres/partials/query.editor.html
@@ -54,6 +54,7 @@ Macros:
 - $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
 - $__unixEpochFilter(column) -&gt;  column &gt;= 1492750877 AND column &lt;= 1492750877
 - $__timeGroup(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300
+- $__timeGroupAlias(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300 AS "time"
 
 Example of group by and order by with $__timeGroup:
 SELECT

From 36d981597ed1e6b22ada8c49884f99b7d02444d0 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Thu, 2 Aug 2018 11:18:21 +0200
Subject: [PATCH 171/380] removed table-panel-link class and add a class white
 to modify table-panel-cell-link class

---
 public/app/plugins/panel/table/renderer.ts     | 18 ++++++------------
 .../plugins/panel/table/specs/renderer.jest.ts |  2 +-
 public/sass/components/_panel_table.scss       |  6 ++++++
 3 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts
index 474e9c89493..e4d3626c3b9 100644
--- a/public/app/plugins/panel/table/renderer.ts
+++ b/public/app/plugins/panel/table/renderer.ts
@@ -214,15 +214,10 @@ export class TableRenderer {
     var style = '';
     var cellClasses = [];
     var cellClass = '';
-    var linkClass = '';
-
-    if (this.colorState.row) {
-      linkClass = 'table-panel-link';
-    }
 
     if (this.colorState.cell) {
       style = ' style="background-color:' + this.colorState.cell + ';color: white"';
-      linkClass = 'table-panel-link';
+      cellClasses.push('white');
       this.colorState.cell = null;
     } else if (this.colorState.value) {
       style = ' style="color:' + this.colorState.value + '"';
@@ -257,13 +252,12 @@ export class TableRenderer {
       var cellTarget = column.style.linkTargetBlank ? '_blank' : '';
 
       cellClasses.push('table-panel-cell-link');
+
+      if (this.colorState.row) {
+        cellClasses.push('white');
+      }
       columnHtml += `
-        <a href="${cellLink}"
-           target="${cellTarget}"
-           data-link-tooltip
-           data-original-title="${cellLinkTooltip}"
-           data-placement="right"
-           class="${linkClass}">
+        <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right">
           ${value}
         </a>
       `;
diff --git a/public/app/plugins/panel/table/specs/renderer.jest.ts b/public/app/plugins/panel/table/specs/renderer.jest.ts
index f1a686fb739..22957d1aa66 100644
--- a/public/app/plugins/panel/table/specs/renderer.jest.ts
+++ b/public/app/plugins/panel/table/specs/renderer.jest.ts
@@ -268,7 +268,7 @@ describe('when rendering table', () => {
       var expectedHtml = `
         <td class="table-panel-cell-link">
           <a href="/dashboard?param=host1&param_1=1230&param_2=40"
-            target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right" class="">
+            target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right">
             host1
           </a>
         </td>
diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss
index 99e91f8ff67..c793cd408b6 100644
--- a/public/sass/components/_panel_table.scss
+++ b/public/sass/components/_panel_table.scss
@@ -87,6 +87,12 @@
         height: 100%;
         display: inline-block;
       }
+
+      &.white {
+        a {
+          color: white;
+        }
+      }
     }
 
     &.cell-highlighted:hover {

From b03e3242e3ee4092ad7cc81219d35a39a2cd6c40 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Thu, 2 Aug 2018 11:21:17 +0200
Subject: [PATCH 172/380] removed table-panel-link class

---
 public/sass/components/_panel_table.scss | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss
index c793cd408b6..fc14236c2b7 100644
--- a/public/sass/components/_panel_table.scss
+++ b/public/sass/components/_panel_table.scss
@@ -139,7 +139,3 @@
   height: 0px;
   line-height: 0px;
 }
-
-.table-panel-link {
-  color: white;
-}

From a8976f6c36005fcbec485f001766560b0767f45f Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 11:43:48 +0200
Subject: [PATCH 173/380] changelog: add notes about closing #12785

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa089b5900b..66ab1906c4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)

From 04fcd2a05481c799176420e802bd73ec24d699a0 Mon Sep 17 00:00:00 2001
From: Mitsuhiro Tanda <mitsuhiro.tanda@gmail.com>
Date: Thu, 2 Aug 2018 18:49:40 +0900
Subject: [PATCH 174/380] add series override option to hide tooltip (#12378)

* add series override option to hide tooltip

* fix test

* invert option

* fix test

* remove initialization
---
 public/app/core/time_series2.ts                       |  4 ++++
 public/app/plugins/panel/graph/graph_tooltip.ts       |  5 +++++
 .../app/plugins/panel/graph/series_overrides_ctrl.ts  |  1 +
 .../plugins/panel/graph/specs/graph_tooltip.jest.ts   | 11 ++++++++++-
 4 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts
index 59729ebc312..f4d0943d52f 100644
--- a/public/app/core/time_series2.ts
+++ b/public/app/core/time_series2.ts
@@ -76,6 +76,7 @@ export default class TimeSeries {
   valueFormater: any;
   stats: any;
   legend: boolean;
+  hideTooltip: boolean;
   allIsNull: boolean;
   allIsZero: boolean;
   decimals: number;
@@ -181,6 +182,9 @@ export default class TimeSeries {
       if (override.legend !== void 0) {
         this.legend = override.legend;
       }
+      if (override.hideTooltip !== void 0) {
+        this.hideTooltip = override.hideTooltip;
+      }
 
       if (override.yaxis !== void 0) {
         this.yaxis = override.yaxis;
diff --git a/public/app/plugins/panel/graph/graph_tooltip.ts b/public/app/plugins/panel/graph/graph_tooltip.ts
index 509d15b8a25..7bbafc453eb 100644
--- a/public/app/plugins/panel/graph/graph_tooltip.ts
+++ b/public/app/plugins/panel/graph/graph_tooltip.ts
@@ -81,6 +81,11 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
         continue;
       }
 
+      if (series.hideTooltip) {
+        results[0].push({ hidden: true, value: 0 });
+        continue;
+      }
+
       hoverIndex = this.findHoverIndexFromData(pos.x, series);
       hoverDistance = pos.x - series.data[hoverIndex][0];
       pointTime = series.data[hoverIndex][0];
diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts
index 5958c80bac9..024c9cac93b 100644
--- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts
+++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts
@@ -152,6 +152,7 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
   $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]);
   $scope.addOverrideOption('Transform', 'transform', ['negative-Y']);
   $scope.addOverrideOption('Legend', 'legend', [true, false]);
+  $scope.addOverrideOption('Hide in tooltip', 'hideTooltip', [true, false]);
   $scope.updateCurrentOverrides();
 }
 
diff --git a/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts b/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts
index 3bc60ed8ea3..baebf2c5930 100644
--- a/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts
+++ b/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts
@@ -68,7 +68,10 @@ describe('findHoverIndexFromData', function() {
 
 describeSharedTooltip('steppedLine false, stack false', function(ctx) {
   ctx.setup(function() {
-    ctx.data = [{ data: [[10, 15], [12, 20]], lines: {} }, { data: [[10, 2], [12, 3]], lines: {} }];
+    ctx.data = [
+      { data: [[10, 15], [12, 20]], lines: {}, hideTooltip: false },
+      { data: [[10, 2], [12, 3]], lines: {}, hideTooltip: false },
+    ];
     ctx.pos = { x: 11 };
   });
 
@@ -105,6 +108,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio
           points: [[10, 15], [12, 20]],
         },
         stack: true,
+        hideTooltip: false,
       },
       {
         data: [[10, 2], [12, 3]],
@@ -114,6 +118,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio
           points: [[10, 2], [12, 3]],
         },
         stack: true,
+        hideTooltip: false,
       },
     ];
     ctx.ctrl.panel.stack = true;
@@ -136,6 +141,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s
           points: [[10, 15], [12, 20]],
         },
         stack: true,
+        hideTooltip: false,
       },
       {
         data: [[10, 2], [12, 3]],
@@ -145,6 +151,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s
           points: [[10, 2], [12, 3]],
         },
         stack: false,
+        hideTooltip: false,
       },
     ];
     ctx.ctrl.panel.stack = true;
@@ -167,6 +174,7 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function
           points: [[10, 15], [12, 20]],
         },
         stack: true,
+        hideTooltip: false,
       },
       {
         data: [[10, 2], [12, 3]],
@@ -176,6 +184,7 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function
           points: [[10, 2], [12, 3]],
         },
         stack: false,
+        hideTooltip: false,
       },
     ];
     ctx.ctrl.panel.stack = true;

From 169fcba52031b104a39b1442edf655e9372541f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Thu, 2 Aug 2018 11:51:41 +0200
Subject: [PATCH 175/380] Update CHANGELOG.md

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66ab1906c4d..22c24f83b7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,8 @@
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
+* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/pull/3341), thx [@mtanda](https://github.com/mtanda)
+
 
 # 5.2.2 (2018-07-25)
 

From 57910549b6b8eb639c6cd36814d3a0850a123bf2 Mon Sep 17 00:00:00 2001
From: andig <cpuidle@gmx.de>
Date: Thu, 2 Aug 2018 12:37:50 +0200
Subject: [PATCH 176/380] Improve iOS and Windows 10 experience (#12769)

* Improve iOS homescreen icon

* Improve Windows10 tile experience

* Remove unused favicon
---
 public/img/apple-touch-icon.png  | Bin 0 -> 15718 bytes
 public/img/browserconfig.xml     |   9 +++++++++
 public/img/mstile-150x150.png    | Bin 0 -> 9010 bytes
 public/views/index.template.html |   7 +++++--
 4 files changed, 14 insertions(+), 2 deletions(-)
 create mode 100644 public/img/apple-touch-icon.png
 create mode 100644 public/img/browserconfig.xml
 create mode 100644 public/img/mstile-150x150.png

diff --git a/public/img/apple-touch-icon.png b/public/img/apple-touch-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..3031d9aa011fb298c06563370d6b669c0d0232c2
GIT binary patch
literal 15718
zcmZ`=V{~OrvyN@swry*YiS0~mp5(-t*v7=3iS6XXwr$%JfAjvlKkn+?U2At&)o!ou
zXIDjjSC&OaAV2^E14EXVlT!Of8~=-NF#l5FNf?fQ1j<VMn>ZL)T|DBuDfGX0GIKe#
zZ(v|PG+<zXKf%CW|78W9fPs0ifq|WwfPo2QfPvvUXSb^g{VRYmQ<Rkg`}!~Bca<go
z%Yk*5|0WH)2TOvE4k;0(4FCfZf|ZvN*8r?t=6GcnOE*3D^!T&``>uW>a|-^_Z6#9`
zlSYDL%8;WFhepSe9&~RrfQ0$3%HE9-R$(NC{)ll9g)1$_QD`Ej@ja2u-o(qwEo)^{
zCFeOlH}~|$JtOlflQ?tx`2Ip!R7EpKplnKAW%0Lq*#H*})_GjEia?iPWLO)l8IhA^
z3)Bs5vhT5B1x<&KI!V?Ag3zzpr;))x0*GOHIZ~LXK>)&Eo;}z;9*-^ls)haDMhO+L
zCDJA=(~RGUx8MWoK}*?G@;fLKBA6K;Q!39(=0lAV%lMnJOj40hY0B4(ZPS!-(7_`E
zL-S#NLopRWK`&k^jE~m4G#wCMV}REi{zh&l(A?)>QU`_$1o~KIqG+0Y`-D6${N$O4
z?Wsir+acSg<TCuZQGH+-E;Gf;@zr5cH~58M)P;Nu=}${sfx2!xpTDZyrP%E#I&Ho<
zoaTnbeOr{F-JvUj3^`zohDs4GRlIM3`5phUVV8yzd5EdcC<tr<BW9Kr=s8ogEPqsk
zxwI#X#M>)Bt18m`s-h3F39V-T*GMSD%k6~jU_LU2Zza=(?3EWNE@953{ha0>cQz%}
zKmT!&x5w)RXEu?u$Pj5U<iwVKk09TQoaZlgIgiiQI&;m?Iw3H9OpHEJEP<52mJ#4)
zn4I};#$=uLH(`)}ZIH+=PKRst;;Q~XKJqJwto^z}xi1u|!np$^YMF4QQfLX>lKI^}
z(6{MBMN_>3yTFO>{ay%$VD7Oz0n(2PH(#tC%GXIRCLec$$&*hkkv5?uv`a)?8T}pF
zy9ZNJMEq$Se=eJ#QBEgYchI;t6=nY^8IA-I^_=Oo^WJ!prM*=qjbRJd<gZG`;|6V=
zc5%C_=nF@&Rn6>!#wc;-Hha{A8_=dlx2Bap86G>Or?FqGu;SdNy1vhMxuch2`w&?p
zL9}OCUWEb-uRau#du%I-4{;39h(V_^c=`bg4>|(4^9aH+l|G|pd&uAtv=ANfG}jqb
z#BDDF6;ict?wCjBdIaWDk|C7bJI4?;1q%Fe{V2qt)Sx@EBOSK^ch($f`*&#}#a++@
zO47qd`LL?_N!`;Jp7g%mcqLi4pwe5l&JKxjz`~qnSQWd|CJHZ{<^9#~SjqX)BHTo0
zdYHa25YSHiOl$&ULdtdk4vf>@Ztb1)XA<JIk)iQRI_RO&3<+>B!-La2MU7o566!t+
z7_YE|&;=)1iK`>2gZLYrDGc?_jN}mdyoAfQ8Vsz^uD_zh4dK<D@PK>56hb}q29bb>
zVt59S^1+=Yk?grHCZVpheBMsm1N;mRjOEkj%OT<~;qRYTqk}F5A%j<Z3m_9kP&%Cp
z&%VpwBIcF_HGwekdSKpSP2>NvZOmZtyb>dKF?Deo@A5DiOAt7-L7u(@?*|=5bd79t
zXb}vlFg-ZQoL&Bz#Wic>$dbW#?{P=R$x>HmGu|r!F^Y#%q=9oP9F8M+Vj=JeHI|FV
z6Kl$#;varu?|I%o!E&yj{0xOV1`%>XxTg%P!pMR(42$_NT9yfvb&!01z{G9Mihj!A
zQE$)FfzA%2b-D1Y9E4VhpsvUYFayoQe18!#9Fl4s1=l$}tpZX#gdW0Y5Z{R#ej#sO
zOn#mby&>kH62UgcR6q;`l)(0X!R@gK&vrdDN*w<;M+XRg$)i3^gU7+-h#bHO@bYc~
zQtPv}I*djfZpdFZ@dEJm@Ybrb-m%Xe-uq+B1F;RgYO=q7mQH$tl36EP&3%3|a<2rS
zoY2Z?Qy3`*arFPib`kPCx$UDX+ztg`Ytp$bc1Hie2xkbq!kx(b{+YXI+3_gL{DX~K
zjG{E|swgd+?ty3Eo^JP3H)8f_q3I%1R$r^C2LXG`4$KGxh30FyX`qiFu@a#pMMBhV
z0D|cfo63xx78iBWEEpy{3mq9x;)p%T@_~XEu6QE+x6~J>X?t5_HIP9?B3HLNrIlC0
ziIZ?Ib{PCxhDZT>f8RhO>jt~@cO7v%6T<w$YxMU)i)ZMlLCfIg<qUUSr7Ve>$-nmk
zv;^!P6i;XuG9ZrPP|$nE0yr1AOAH=L5K^L+WyP1WiLm&M(n|e<O|SZSd~<+V<wL=x
zySO>ubv5nXNwuIYJsvnXD_rq?AZB1@(~(o7aEqn_+gLrVogAl<J`;`}+`ApaLstKB
zgXf_D4zqrCLI&$GN*^B-06Z>$l2gl}V4>BB@=Le3iwG&O8LYw}AC~-O{pwKBpOm0t
zt^nrOUfA$-vOCIL8>sw=V{u%9J<GJ;g5iwrv_(-5;@z+CTt~aoxEqZV@q6ueS5`=)
zAOJi558LW8<}{^=R&G30GRL74@lIGBl!2%)=v7f$u>jmgN$qOm*b~%4-k64<3K&m3
zMbd1%d&5ni6DtlEDvU5AG8c|LJdW{F<TeP#LrjArm~SRbgnCf?_=$4~vD|ZW3HLtB
zQkje^1?LmM*%iU(%+xU*$XCfwxQs&su}bfJ9BQr5cDe`5aF)W}$eDZBNX8%(SE*l&
z$3A`%lws7dV&-X{!l&u@7~gQNF;ztGkGO{SUDQr3a_F#&SP(Yn@Jvt+bP?RsEPRsK
zG8^+WA3|%f<_4I(7Q&M_vmpYH%t-9QXVWwl0}kp^H0`!%qx}~fV}mr{?l7&Qs-q<k
z$#6p8IvkmS;kNd5p{I7z0Zi+awr%CA$RFhbDp=Q&D^btx&rb^xu0Ix}3npX2(r7@z
zF1S3oK1v*y?eU3TrdvTIwU1juu)=&Ur6=t%1=M&KsK3KCojDxpJW4Nv#Pdn<$o8+=
zh|S+-uEnL`07$Zy!wQ_4(gnI=j56~xT{5;Cy0Jme%wfXZv!6~Ls@BOfVi;1Zr<XeR
z1x*G)gjK}tVU2HrRd{xRw1L<rc0tG>pAck{W_~$v8*%HM9~rS4bE5}=8N2&<oIeG`
z5z(SY`3Qw!SE6H5f2R^r=Yeg7CZg3ol}j_Vo@c72uz4=X7D$Ew2x7WTQKIpyE0`No
zn0Z>r&mF?23N{fd`vG`=F>iSLKZA66OS|1Ua#E6+-ygGXYdg_(z-v!WeiSE#ObwcU
zBeBC>Lv+bk3J05(WvcI+gy-H9E=w3p>bBSu2Ct)CB5sO!!OUOqcp<42a3_ha`lfR-
zEz$mmXm^xIWFhktWyfVl+wxsuB`*kh2o#tw*<rW$v@ZN4#CK2rclB-%Il}{^H3V8V
zFfIAC*D5Am27kkLYwO(kONntXxMS#XPD{!V>(zayej<psM%rMS<!Dh`wwNtf%CiE$
z2MRbssGr@ymWrg~yFDP#A!|B_u9#WCWrH<hQ|-(O5hvN1tz`XkP32(Hk?TUZuL}Rh
zkEzSbeDiO`dW4^!OhLTNdrCI5a8s5}Y*RQf=Es=+Z%>FGNPqhw2b+>Sn|yZPp^xMc
zryB4EkG_BFH92Ed#wVYbr7?WwHbOlpgl=C!5C)&%2J-&X*ThbnSdwT#r6Ar2Vn|$$
z7aZ|?NE~|TOsIGj+_M9JoA*K_eWh))&$8XoSa9|tj?gYn?`O+*@Y^Xd0hn9M9cc?D
znj#7K)iSoX0Dwp+7h8?8e#2Sm%%&_egiVlh3;5e&v@(_?CL0SWs1^5bnl7MBQBP&%
zBqQMX080BeH4l+UUUz5}m%~Y3hzrpizXg_{5xo<3{dVMP08R3G4`DR->N~LA&giS1
z{voRfm(m)=xdkkIxxO!%FOiFrg!}KGOvCLwqK&n=1*KtP6&1DEv+%NXR<jnJY!6c(
zqd+560o*#&{pC!}j4pmScNGro`kmZ0J(06zNzyJTPEtPTZI~%~*-j&oHZ~YR_XU|?
zBp<fw!He;w?Rz}rZ%ZCdepVM95KAO&DeiRCX3eyF!oj=+ceet6tUHX&vm+ZRF5h$j
z&tX}MEsl=gMINYv!_XRfq}h=U9eSHiLc=d-7Ux)hM04kY0C}0B(*vUI{vC(91ruP1
zSGuD~Z(14XB3FK|Az@wkt$7n;mNqE~ZwV2okRmB4^WStd-x4ciHnCothuh)(2soNo
zx+RS4h$0){7XLzNi*K91&&BXMZsvY=^F9je$J6HqJagimT1c|p({KVQS9nKO)?`ii
zzfu$4ZYW(WC!lNWoMo7&xoXZOfX{c-WKSvXjFRjrcET_?eP^71l+6sFedqYAzR^hH
zeA=F=yxInN$TAO^$Ff7yLXw6j&LjTm8=1MPvFsFI*_IsmH_e@1$}5hWz%8GUhW5GX
znP4gV!)EE-Qk|Sxhtel5AT67l_v5hV$Ff6SysBufxM$i-9~S{Y%pu|!%l+4p_z&pg
zvia`Oxu)C9>Iw<OjMa@b)OQ+S&4ntWrebl-gFTgZlSnW0si(<PCR<K7R0HvR_>gST
zeeR<Dl}=zf3D<=^X9tlvn9ON3vk_@@+x}jp4$aSNK2_~dqdGV24@luaVX@G^NZfAz
z)0rAt`j%Hwd6|AR#HBrjphvGw&P~Sj{*8`Y(@#T2cBg3osXy2ShRHD9dOh|7>DJrJ
z!j9N%JHtaX36v8_KMBn~4ZmZ|V{Hz@O!%#Lo^k-Qv$*Rgm2`eyR7SwG?ay<0+IeHH
z2E?GrA_lQeI6k+nJMFva<EGdO7LV;N>xiI62af$}YojO*sqN_igU}cm4`q|F7^hOn
zfJ_7X+T3oKuRs4OGP{++xb6<U70<5}G!<Wml5o-wSatwTTRV|z3!hcb5WafNV4)&i
zq=U4;bTdQa5j;<3Gu|jJXK>o`U5Rhnbaq~kR3sK&0<b4KfY3=LldT9&*TZ+I?Ghc=
z)ThiLqv9>E5vtIQW0ITLuOQgw#HZYub<!YF2%RZg1NxNn0@<dr>RK}F)q-oz3rrLJ
zqeYPYgZ*%4hxQ)P7JoIkh{V)Qe@Y9UK9Y-`Bt=~S1zv}qvK;8ASguZ?OUI~x6x=MD
zn{HK9>zMF(7nNqJBh`o83yl^ysap`x_IM~V`s%(WZc0!)>EUnhjKB4b&49Aza<k&$
z15IAaxsgcoU>dyYNp;7P2@l*J&VVRKYKtmeA$AF=>cnhHGInXu32b);5t9F<@WqSV
z<vV;S_`d_VG?VG+_s?|`V|MoMh8^ppwhz;)6Y-H(P%<~XuXBRhfgesOg$&m%*mRtU
zQB@MV7O+(P&s9>H5UVO6<5X(}{pB1+8fBzu3Z_;+?jWv{$uH6V?(iGQdRA*M*DD)T
zdD=;9`&}nil8>occ=eO&b!|?{EwhO81Yx5UNYt7yR7N6nr-u5mh*4Lc^@4GD{=)-W
z*%xjKp$nIZ?w>_gTbJ|6AfLkXCrv1plf+w_T4OBF?>LK7amm0WLEH%&;tz5IOyE!y
zlH7;njcAgu)+iNux@P-@FyZWWp(6D)d-k&H7~v^46(Ll&fY)qH`1~pS;iqo&)Vj7=
zkO$$X#7<(TC$9G;*TNMn->DpLSO&nuLpiE%NShVh+-pDu4farE{C<d`RlB<Wh4a|#
znRYzDHTwC9#zb+MwJfa5yPy3Y91|z{moB85pc<o00|-AqCS8kwJwi8rx8;}H$6fJ>
zN(r^>vG6%fY`9h2g@ueA;%|#PEcx_gHyAUE4#H(%o`|BE$rfwH_8iYHLf7shB0PZ}
zTyod3+^fKkr^;0B+nCE298N*V)1|3jUYi9n&lR|dswTYi^5f3UH73R;PA0z#Jj3G7
zzE5c{fe`0lLQJ|qFa@cW@AI{^y}59)*z65Cr$b@21vT;fEzACd8uXO`ix5bpUM!z3
z@}#9wT!Asc>v|J$n6GZb%!^(^nAJ02=kdBf@g1hxt`)K&Bzt=suo)Jmj~4{as-_ej
z2Y+O)Y8HeZBOtqP*4y!)c(Gph(Z=f7RU)^A{ETayu&D-u=ZsyQj_U&ci~t>d%oAe1
z0kBs{(NHs5M2jbD6iP5hH?MTq$$hIuE!i!X@cEu02P*2LjnOY1!~Dcf7>SEcd}*W$
zmPxuIbc)!f$z9uJGDkjVo1(s-s0q8R%7i)Ilu<nYn)>CLv*mqRhj<$nJ<go!&a_8m
zIt|%`D&1R?<rQ%xD$s=tF%9+{;@JCMBiG?EFzNMK*ezN2ILr#O4_8nDjTlCX)zq=M
zSGSDlBM_jvxz#Vv4{+<vnKl8{km!EES{M%|#?*78P6FI;C0r*wRF8xS>lKf)p2EHb
z^Jazyu=UtYKrNGWBrMikxCS-KQaF<)@6%YM!^g>Fh0a)evs%?mE1rufBfG9i#&J@=
zktp|&ndY&r2HC~y;~4|rgX`+G&_fF9C^NfJS6dQ76CJL>PRR;{WiZ+N)O@T1n`(5#
zI{87JIn<{Jl_tM@Gpn05s_sRM`JbT(Coi!0H<xc4MswgC`CQ=BlnQn1S)AKv1cUrW
zE)9l2f)-&8=$*Ke$<YR=OswO!n7mo5nrV$aIwq$tyvrg}Bd?e^LF~qJ@u{okc!S2)
zme*+;hs2^4R}}&_vXsMFL|bn#*+!8O`(d`pU4=epYSXU3{cJ9thxGx&FyWJr0}`0o
zkE}sJgV^oLxO2OLvhh$IDGIOA7JcuVbMD4lsPMDgOJAMTp5N4IS#_`Bo|$6}@-XXc
z80OHmZ(g*wXaEeZHF5nonxMiBE*UXTh640?Tm_5PELH9!GZiz8k-?rzFN4YA;E``G
z_=%)vPBiq#y<=8JLUrhh^BzUJiUCx;CoQn2*bZt~jRByT(ia!(MY!8luB551MRREu
ziOP2h1M!8zYNsEtx6-v!h?X^M-jdP!8|w?>jdriMldN1BF1c_fo|Gq?%-q>0aiH)I
zL9Jdu>_q@&H%S1;zR30P5h0sVrKk-!M>uAO;H_q{3MG!#o~hEi)fbayX+?K-K_8Ks
zsC+vAuorc1&+cP%<L<#4o#*oO($2vv=yTnIkm3b*94)$nSVkpEqGi&7Dpg?+Eu362
zM(M~ETHmKIq#@$CgvAkIZ*3qe@;WC^z!KdpiwNf@Yf7uap3*2r==);)b1<V|FI}rK
z^oNXm8Cf;;;40oEq4J+^t$kGF@`$8j8))D1aEPX>Jb_Eabd8v;pvg2ou6s$z{jN%t
zu*5D=j_!s;#X{=|!wixA={lHJY|f((7c*~zfH{ag@uaGdW&>sI9X=*b8BKA&^8ujI
zDppDf`)vAr&*-ftzlixtXKWeE-f_baZ5bYTB&Y;bE!FJ!GtYDld@nFf=5|K)p7DHX
zfae}gGZK=@6O&(r@ZeMQARas-yUAa}SB1>}WNdPp^rx^^St~Jh-E9*h;G{WWC~TnO
z=cm52SnhS7vth+Sj%z1l)^ouZem5?8MK=5%n_p_OFx({8>ocg$$^0ohooID1Og%kD
zSIdyMTBZQ@`isiN4eMI@qOM3ZyRN*dzFoOh!Yi*cPzNn=-$V*%IiZ0D%(t#g-z-Zy
z*G<32FJTiV6WY@nT-bK$AE-ib<ZFW*H1FKW_>1o9i(F@gPU^kC$k8c?5QeDQnSRI0
z!XA^K;3@wiJuWh@4;!(p@LG*(7XGu2@0T+EFdCI39Z50L8>xvK`n6dkgk?q4_lti!
zrZ^aVo?`+)JaLuIOAYNuJHw-w(dTZMfn7c91WmJ-r#NNlO&dl!1TdAWSF>L*CieNF
zOTjg!u!h4s%BDvnZrt4e=Eo(=WY}`NL;cEavLh2F@s8KXRb;;Kwuu%uPF5tTtA&BI
z6(#X^wbs?QP-H!Yzik^xu{}ypa@L=t36Tws_JDqf&N)Mnl?#!<Y_kFFmLj7s{905#
zv9xH`lX$R`qvrb$94LFA%@rTpc=YHFk#QnZ4!#NXuqPyb94$`v?7s~v96U{3Xoi$M
z<0Z^N2DG6k?$6v*1h1BcU1kz1Yk)OhBt^L^-^{pzsI6j!Ep^aP0p(6Ej8=#P2VMde
zK^ocwbbDY)eo2Hf$1!ePx+hmuk>M5VUmufQ!HBkPCjkd?w;JM2wwiO=^Oe8t&|p1%
zx7x+W$3k)P8jBw)<q|al-H^C^z9-KR(Yd6!2%Ju<+WZjA>?vp_4|4A;*<?MjWyZAG
zW`O%)h3t?C>`BQ6-&8{I*qf`MMazwFmvqwrF7s5*SHBbWB;R8dj_%2GW6^lFR5hk8
zG)Y@9ffN^RVVxTej^mOE>DDN>{iJ$r|Bs>{0c3lOfUjmTAN#}eqq>Jz)!c_elPkrv
z>h%ZEA7i+2RqH`C#6Owq|0>8k6~VcZ=NT!}!khXQWU3pNFM*YnD5UahbszJ`W(K}-
zAguw_gLr-1dC8eh_Z9h#S4hR=#c}RUBy5w2H<|-eyCcvd%v;nb4_nFh<m}XcGq|kl
z^eE$=lj@T4bkRbYYMr!PgzVngka^FD%E(qZPsJy|Q5`Mjb)($@67nQSGjKENfrSG$
z%<|L=289p&ulFejjYa`>1fYl@97_KOO(C#}t~ZVkN;DTmeUsk!S47%j?NjXiN}uOs
z!KFP1c@@L5FhK=-x4EV0#pCbPN%4vH>d~`-r$30`C++?YS_Hy`TWnvbmY?DUj0u9u
zd-glf8fq7=#wi@es=e^y`DLA}!#vsDCi(YSXT-mrI$aP6RrJC3&<FB@v=4+FKNgG5
z8*Ssgn~Rmcm%`^@`5>{{Nixy%)6-}Bik-^K?+#d>w8*1!Pg*PR(#ZG)&X=QWgY26w
z;sJ=6C3}03hTe7(5v_w3PI7BI)CGvz7f<`KOWxlcniV~aTH>b8+nL?<mMH^5hx6>~
z5z$nzM!RO#FsR?uY-Dm_<($&zZHSjEf6Skz;!3*iLKg9-ov^#e25n@aY`Q6T7D(U<
z6(C#^@mV3FT_y%kG*!q<y;k*AZi=Nr0I859VPMYIC2IoIap~t!PMIt^jtAQr4p^go
zp|7Q%G@U-}CrEUhFKM<tJp7b1F57*W$E%HC!Oy!IY6v_L_G|}C<X4Y97_@p1(uaZ3
z1L3?~HlN~9aaWvww!1z)Xj|CDFf4jxnr);UalBk`TXnosb96#+_-qoT)<<kLr^dc-
zDQIdr^4qv-*J-p0@c25Um^WqC9fZA-h!Q1JlDD*A(jH%HP3ywP=%tP`Y1u=*{Twx>
zDtjc>R+)fVnj)>HWV50H4wzp<yhYSLN_U*lCwNdfNj0vs^-R0WL2vm+Gx~*Dp|kuo
z6fHnyNBoKd2*r#1;q%L43*7=78F17g==`j-(XrJ%kcYX09EuT|HmCQ3X2L9nl0OG6
znLIX$I`Ma5at|cB(Nd(bWAPLK<Ps0*r*|!3Z(OcFmf2nf{JH92J*GZp2?PQf<H#Na
znehE5cXA?z!_IZ(pF%6*CF|pNJ_bN-%h2E8d3t`o60=S5OCYu=ZirGysv-EvLFYZe
zt6S3#Ep;Q0(KWjMOjxJZ8TjxO*Od>QopI9-f7i15-F~;f4=$2<A(oxskyIP}N~e+-
zPH2htmm>Ge)Ae^8>9>vQbBT$zQo=oPyvAWisI9n*T{TzIBnRD9*_*AO4bOTUZ4<#~
zR>NC!RC~xU8=^u~7{YGK0%ZI%f;v3cKj|JyIR(K<?ef@<zjLq@p*sOSN!l*TO5JtX
z6jJLr+<%G~eONg%jYwJVK}SF$oF`NP8}(_lfvd|A`mR?{&l9RAe`odHpQxe+uOYW0
zFLuq^v>D&fKivSLa>`Edh6=y7)Hb2&?qvztIhEc?Twv?V0@s^2-g$90Tbt~+{#}|e
zt)_N+IoG6^0CB-Ph_3HcMKe-@1-K6b-d!$J2Ss#FTTpXa+ymxX)AyDg(^qDmzBH~x
zu^GC2LB!R-x|%^!<S*T0`{Rvk6{DGSiHR3+UY(>F=pN~8r@h~5iid|kMEe4^nptj3
zL3?lVH!lq57AzK1>bO<x4Q>+`cut=_#-YZ_9?^tF15wEsjb84F%GCPQIVb}0N(m{Z
zUrB|3uvhHE=QT~oC*|!jx};hDif0fM@BfK?G>QLaH4U4=c={EXZd;DA`mLRM2GTRG
z;QYwn=?-{AR~g`jn{)qbrQUzXz1WFjts=?q#}@dT#LhhFBg>IhK#f1E+a&O$Cz~h!
z%g2}Hl+7bQyF0`k-;7iy+q;N0;;Mi?hgQ&nP~OgFfQHgS*7##+CX>&XN&G@sDd4ZE
z{HOn}<cC?3XRv0dx3))v%RX4aUt^1HusO}qZj5UP`LZyUr)-~^%_WTj-cAn^!*5h9
zfW$8Gxuzr|*4YffqxzA$A6J?bBC=AQk$d^41Y~YQWI%(SDn|)*%jFU92rb6-i2eX4
zX-{b;ah2m|ugNX`=&9@K{vT7>FEI5T73qHRTMd|Fo9MJt_BLko>+NS+Us+6kqApJc
zM*|o?kJcehg?4;vpVTT!GvBoK{GVhN*j6Y$$NI@K#g07s$L7}~1|ii>AG%F$U2WXX
z04*&ypQ9*6-<c<3$fkVmebHSK^7|RayRojtWQM<GH#W39D@bkPzzuCmdi69#^TUVJ
zUbre7C;i1+&7p%{;vMT;T;;={WY3QDJ!e#+NtV>!oLShA+Xjh2nzbl&&3*Svjvx%y
zshn<OXg0WjIBBi_H3OUz@nfwg)&J2ZD-v)0{E=vc!B=;b{rWJsuXPgk)AT59q^*W>
z%aVe9VrE8Z*}%UNDfRpJA}O*Ih{5R`Q@ay_cHa7*nzXHAP`4&DW}j>y5$!hR{xqMF
z7rbFsQ(iRFM?!k;26aI?LTMZ>&}N#d`WicivzJ>;W&1D^hz2<{nEPrc%*Ru-4|)t`
zHHz63yLUbZ<Q8EqFA9Gmc=<bY+cfgsQf^^K5jbi%S$*Npp!j#Qv#5@ck{MK&mAWp(
z<!aKtbxB4}#IO;IP`DJ;*J}q|jNd9-bdaV+*w|3~1G{cB6gOY1SvvcmS$5PSF~e}f
zrGBD<Mn2H@fe(HPbUs&#N$3kBeTtvKnx)CrxM3$;a!Xml`y~nK23zQ;P~wu-)jqrj
zhR`i_xl%%Kz15Qf2#0rgi*c!I#H$?qxwufZSHw-Eo-y(TEuBCTn|CJKXWfCw7aIQY
z0Pg!Lv=}0ERe<OlO?tg6+q(^;a0kz>617*u0tCKsct&K{Tv`SaC(U{!U;(q<&sp~4
zOoO)b)^$V)=EE4tU(mH0nctV=k7s&{7?B8GsbL|CdXy^TL)x=@&;lNxS5Pspf@_JA
z-QhQv&}W2>_7e_StR6lGnb8HEXvrBxQ)^$k9}w_bS#%Cy`iMhU+R9zz!)U{FS3c-1
za)49~y+4j#k&=5X)>Xsl?ivVPR~v2aPTLF?Rti46^?=;?di6+>#?B-h#Qg8Gqmgg+
zgQv1-Mpjvs*!ZOD9gdg3R79;#dHYzpCkN6ZQQXxPCq6yBhF+#_^#{TVd6-5857+$Y
z!GUb6!tRnnCZCm0>GJJpR?O0IFZ}*J*n5+QY^1{eES>;*z>zD_l&m!^l_jHdc8jmF
zApxL#cP`>H=3(q^>ss=$MBO91L>UaJf8sL|;7i4Xzq*TC<lu#w0|Ok1yz+8iYOL1$
zuTv)b%@GqsBJm<WxXxTy`X$51@+vvBx%|kPU4tS9&l(LmB(1=Z9Ud5`fedg=+`V;<
zwkW8@65Hl`yKuTO?VHpP{Fn1+dE$iy&8$Gps_vJr`<tyjmeS`${*h9qA$4g&D}VfP
zwGaBXzjQ>-vWAkci9E8x%;g<w88By>sqhk{A&uNTp#?9)V@*@mYu{FkAa~=yj&0jj
zs{fGavG1Dcv79<s@CIvnYXaf?s(c*Xm>Pb@muZyAX9y*1Me1lnUr(|O#T*elOJK%Q
zZ?DMwgF*>r^@}{WCp)=+E%JCONQZ9Sk>+H)wDE1lpV0A6#S*<8#kIKB6e6d2_kt;=
z7{)!ZeMl>dTPfWtRK~*F0b#7It>v;V<j1~ga6YZ@9CI;d5HM~XChpo$7WQlm#q4>O
zX>_zOIyY#T@1|&;G+k#Zx~UUE`-Bk5;xH0;b8gBFLQ;6S;u3Qi?=q;lQ=BR?Imd-g
zMami*zD4eSZg0wSAc4Oju3n12Byz}}QMFUy2t<}`vsWB>Wa{9BIY{&9FulQTh2LT6
z1-p8tw`46iE=rjYL60%GtDB2$o7nQwZn4N6=$b*jxP;L2P~#dQO|F-xl&-xcK3D}k
zxlGNrd42F4$O$pkk(&t`Ug31`^ahEOtywB{rS?JiqdzDV-<f}ty;1q?IW1V!H5mD%
z$BLg~f6M$eEB^Lo9Fe^h0dRZ_yH{AYp4VomC>Np~E)Z;aIt_2Ru2kG7*qLS6DTs1|
z7{1fUu)X<nUAozBeo(;~=Q%w-Pd2NukJ8>K(a~!DR?@RBxrfAX$+LRMBG}r-%>GuX
zor((L*Lo;xMn_z*sy9xG^wp>q35OA{bz+LByOg{?Q_F)FFV*(oUI>NKeGVd^P%Bu@
z7X5V;28LNPSm$+k>(Hy|8e>Rd+~!(AV2qpkNHo}}QbH$aKG;`-x_2n<Nmx(IaMO);
zdeEC|q-%TjG$&aNijB~TT{D!_{)Vn=;6|aW!mvqyx<Ed6i2WzxEjZ}<<^$tJ0a-p@
zAhfiWsYy{_WAMzzh;1&-2f@W$dTY=oG7bL?E$^@M2KPQszan9O89<tfz@g)qBvO@9
zis9jV7m6&A&M%iM>p8pZhp=&PZvmwir?&i}QiyT0C0t$%R16q{XdTs{=YZt-4wr7z
z^YG)P5|PKD1ob6@;!*-+M7x?vDY}OvRIdb%yaYaS%=(IrHQxe<wql2jxn@O*6cZ$E
z_REl*`5=~Z^AmK$y58RK#1FVk&7BXV17-UN?%Jb}s>cd-;+qF(@ufpA@Ou2krVdSO
zuTX1iZEs}Ng|#X19VQt^u5MeDwBMc)Xg}<0icbztI+7aK(TA@$_{(ES%c~%ja4wBE
zVzLN@R-V60N6U5$(8BuNCE!Pvwl9<!StG95c)W>Ql9miDTn!RyCGC!sHAyB)I3dpe
zgPb{Ap<KL1C9M?<8C+DLrz@CiF#J+qnB#t}X00%54VRnz)<D2T&6RnA9%x4J*p{6u
z3EOsB4}J6=Jp(Tbba-!q9~RMU(JkqZCW7R9JrkifcnYn0ct+ka6Edy84}7C^lR<tZ
ze~pce;U!9p!}y^k@!ORcC{IMVFuU8cuc&`30(Hwd;6;ZtrS|UO{kzx+_<n!cfw;l}
zJ+vSx?rYhbFvu9KfcdU=nfoH>AjvrLOH{1`dvoNu=zQ+7_y^xUdhIg>F#p1;lqf{C
zym;Wya^$d*Xz&@r!s;i^)Vh!92Ugh!Ct)J{N!J&}-q)X<4>sE85C=vzTE&4D|4tFi
z<t*|B-!4`&e-u<zUrjpN>$8OY{*k9R=s3O6e2Z|=<GolNq1fb4D9>t(o+NvV;U>Bj
zEPU*g>|Hj<*<W;3d=9&Q+TXh?q&fn-AAI?V^_NG<XFNw`f-`tBFZxFu39Ieow^o-U
zuUlJ1&L`3wURRhLD)tq5wd_AuhzNdR^0`EC+y~A!brfk3@<%Sq8C(szg^1aIp8N%#
zMQ!g9liCau?Cq=~-eSuS<E>KmAr+G&PRyXNAL0t_ZRE;R{`hRP{~J@#H2scg%z4VB
zXkHGo2VK4w^EA>n6Zru_6UyS1c0B;h$AM-Q_rSXJVka=xX$@+1Q~CbR<M*eq5A0jN
z5JF+cP&U!?WI~3ywTDs9<_`a(pUn4EalH>hJ1FsAp=9<w0JlL7hP(UQwv1Jz<BdnT
zVI;yeE-DvuDzvCOY=)K(jhU%2nyHa}qaEVlVSQE-<G{&3u<7ff{IE)hiT`Y#U}nTv
zyi}o&xk&h<pD32WY%I7{`K4EHdZLO=epsW<<1P7yn&39_Tzwo?eV}r_?OpC;=>5?3
z*y*}F%I*fsj!9`KOzH?FkAFtH{b}k2){?op&DIX|t*;RWC(c{TqJQh-r+c`I%qOl>
znwtfUf_gJ~OsR)PL6Q!6L;(+r4tW<~fZTy^68Exe)Ot^p#PTj3RvN~X4!`EF!ZuBE
zy~b?R^|4vq=wob~8~69Te~Q?<DSyTBk!ENP$l*gk`?0mz64Iw|q*l|YfL{*(!>3^b
z96qG{fHr|SxYPY_4+hyl(LF;keeO=wMIgw8`-W--VTRN40p+&@p*6diiV3MnwJQ;4
z5OGEh!p0VlvPg@u5TOl?<p-&l?{782^hCd@kEG6UJY%J>Ps}7Wx8c9k#E%aFZ-Z`Z
zlq{!Ba(M|Ws0d-;ZP1Kgt0M`Nw1$o~_WTfIvl|lN(udJlmbp*s*yNvn!9lvD+Z;4O
zu`v1~+_1GU;lQeya4M8OXK!Q!;_Ea@v-ELb(8WCHYv<B;VCk0Op;=`T+Ppd05iBmk
zzJa|Nh;<FZ*|CqzYck?$t{E@4QgFyyi6Q!Z<h=f9QbuTCW%j+~A|CxqR`zJxpzW}n
zOvN#~(KN`C7XL5~Ncd@7gLq&xkL6_YC_TAvdtuA=w*LzphH$g_97%9+2a!hW=vI&T
zx?U-RTYChk%&$WE7LoZu8{~jI+&1sKUUp>Zo10@~x%U#v4$I64#3g^jIEU04vFml>
zx1<y~nZ&VwdswL?+28XSE)$gB*M~g)hVcb{VZ&eJvO6Qj-#=Wgx^nX?i?faHwG*G(
zE{8W<!{xIeP$7T^0C@xWw{Jz>7)O+eZ1z?zfQ3Qe#$VI6@j}$7TCT7ObvHw7QJx!5
z09!IY@_GZt&rSP%--d@pi`Sk`+whhSiWw8Pz@!4G-Of>F9jsm?Pw9AE0jQG~MOB75
z#vAQE#0{)l0)KqsfrF+Wp$^3o>f;jwpZNjleAw30RlZp46?aC|DGWVA4jpshpWBA{
zxy|u$+Zf+H3`J4T)!e|`CS4V&QPTN_W_m$p`M(uDTOi{zZxw6vUK#`V5)?mI9rkVz
zhmEJJ@@#0GT_!G4ld7q$sx@ML$AuY}uPC1{Rqvk&ifPwHc-qq&XcVp2MMVia#g-!4
ziUPeL$EokS&r32!TLKPL@b4J;?yk}^lr*Wi@=5oL(vIv9Zi=w=m}?*B8*Zu4>sk?m
z2PJ^;PdqD#I&zj6nPO05M7BMcI;KNKmQS>`J>dloNwurEvDLU0KQGfKz^!{vfx+ss
z#k!kR-2{hIDizs{X&=``?9h4FP2vKg?Nw*(%TfgmEAOY47?C)A&b8m7x)5%)dQCww
zW{?Ifia5RQFK;iw1ne-_4L{KPIFXhzU6kDlUydBQ^iSUbe$958(U-=Ky+@Z)6~`l;
z+1_~x4Nxp_Y@~V_SEy#<3Y&tHYXmkQ8Y+fob%zZNHfq6Cr}N=ycUBhEMAXj{Jtdl+
zQcts&xyD9UdLhErLw124m!wtQ{l9-ppdudbH^3k9RW7el#fb?R))KMTB%G=_*?Jdk
z%5BXdSzFxF)FbuppmY91yd)iPl{RL~wTGIgr=fKy{?-i?yr?u7&y?3#`L|oIYO8s=
z=AoKohqiV<2Fq)mFnRiVi|m?UjB+b{g2@kq)kqMLEhI5!+cIc4W~lz{T(GJE0L;D2
zpHK#pt4Mu386TIXCx<46&BZS;BA=5<6Y}Bqle<zUAx(y!fytlQI{CFlV{OWY*aEnM
zJu@;A>-Y~1$7Ejl7USrwJdG_TB-{cZYx~-~kS^H9_QQ~A<&v$Z7k(-N^3;PSjBV^J
z;Z+X^67B}`AnZj?9?a62ww2p9{6>TA@wj=*rjfQ1ylHd+v^re2ya|LAsrG#Q738oS
z)Jn$>u@78BPip&@3!**bKA<2-UK9!?`$7QO6jYl`y*YRHc@{?e??;pB%3Y`~o-2na
zB@W^i!)Y`KudpG7R5yLvT9mipKB(+KYCPcc{CNpAcY91E+Prv<XPV!ut$10<Gw03}
zSMe&D^Q)nqI|QD;vzuU|+l0}LWo6I&)}+i-RKinRvwKE5eJEjAjAj7dwS9vKmWQ{c
z{sr+d^1P1Gv_{%xPUaWlM%jV;#)^f=QpFvKI_4W~zlHC5`dY=FRl4d46Hl2Z24e8!
z)<wk#PptR+MR~O;ID$)Ka`naK`t4nYowH!KK_2KdIhJ+{*J`+=f$b&J)mI~~g$!|Q
zyxh^1HB-jSlL^jJ>wLO<oc^I6l@{I)ZtDulU+l_qGqM9EZC?TR_;M@s+dAte_K|E;
z|Iyd-RHv{Wo3)EFu7W^(_C0k`k1LOOy*A5-EYe#&;z~6Gw?{g@B(B-;4>z`k&Fp!P
z2wWV^wyfH})RSwWMl{s3kOqglidC19spre6Gmiyp)nnhd+F5@|S4bfU5VgYrVDUxI
ztm*cKGkzG4NtVGuf?_S7GShT{pC0E&J)Cc%75abIJg{FY8k|``ouc@sf)R<1^)$**
zk_v`x4oCn{tzyC^IF<f3FL$cXzU*2?oqo(i{w29PyfZ@TM9Jb9@3ZdU8^fa-V8_Yw
z3Hg4v38kgz@O^gFC!NsMV%BF%OXB`#{Vyd0_JNBC6DfbCxIQcFYyI1g6UB&hx!0f>
zE9A7U<EGl(Ii6As9{-Y@+Wt28xwxdi=zEU`L%$h4unP%RWe-Ocjk$c`(N)OaVoYft
z86R`pLFK9Vp1ULh|JHn7Xy&BX)Q8x~p?4F@1@oA9Z%T5YX1l08!b3z)pdaQvZ!4(z
zr_JnZw?i^>Y5mEIjG#@;YS6sC_%V32%<$XH<m?FX*tkf8KsV-vX-J;w#>q||jOqB>
zP&Z2LIOD^Y5!CQxp5)rPBGm_49WWOZ1*H>uf9b5Jz}T%&JZ=Kk01RU6TqSlqTypSl
zX>07Z+Ut56Gd<Q`XVcw)uEP!4iU)8f;L%<$zI_tpPVllo&Ed({e#;L43*g!}a{8R!
zf@v;BKIf|m*butDLqM*5tmpT=4~9B66ae7NEsw0`;SPTH=--=-qP&|tfqrYk7lpWz
zOEhAw)xFN$Fi!w>3*%n*g9^IPoFJ#bJQg#a3pc!}e{I5SWStsTqOO=(PMcH?u9lLB
zu9(9hZt?n4SEofAB6MwMLCf?NtB{GnEgWm)*O`I=25Je}98)jqhKM^JpId~b#|3GX
z+jzg0?IG?X$((`m*)o<^Q&&pw2B-A*pU6o-HhdJew=O?S8Ne7F7ih|<C4^mZ$`B!2
zv-(q$Fk<Y0RMH659ns%m<r3!Eyct7uCOo+*TYr5E&Sbd7D*4H^4rD3F2gL^upw1*_
zGgmuczsnsY8-i8u^4pH!=G7bTXkN@dR`AGEJ8APp31h}FK<V_UREjs8z4r?qCxdo}
z41jqPzDdXwj%r*5o_kD%GhgHRU3XosR{`z^f^~{!40_$fS)t=-_7^mHw5|b05BAP@
zj85U7q==Wo5tq!z%AMYBWAt2N(}xXg=1%;ED4k4B5JryXW^pA`3@h5vV~i;}yEuL_
zN5#?)oNL>N_7Xyow;8?jBU+|<zcT6SFcYabz3X=kmrB8y!e*h|$<s1zoO-)K;1SZn
zncVxr>3LB&3BfNdx3smKV)0ojvv*UG+t6Ip*sp@{eEU6C=%wBT+11c#()3Am4TNpZ
zUI3{gnI}o?vGFa9fsf$yfsvRl*g$cPiX{CY5waXit#Wv4@Fws{JkG$OKPTF^bJ(SX
zg4fMUF)icV8}{AtX)P*K0{{h**V`wmY)6S8hoT9iO7hf^uN>)SRf;?M<1h40b^q>&
zyD01Q!n`(Gam+$`m0Q=(`n~L;B4f@iS&r2F&FI@Ui;Pb0xH6BV6#UY13CLBGIMx)M
z?4BsY64Ay&S=d#KJALKB=~3;dM9@#_Mk=#g)7I?l)Ex@f#hID`Ak@bOhbyV}IIww3
zY;A%?CF~PeM|P#Z-Cb3GaUMbcdq%&;x)6AdGx>}A{6{JI{igGELE-cd?qpCoh^s$S
zF-3jjJhi}ZmzYsSw5Xi=6g5CdMr9k%#j#=lX!^l)@6q{BYvl04^4*ey5>IKd1g#me
z`=o6~G@nUvuk81+oO}We1>UsdqRjFXWqq46j}LEF1@n!Q*!VG<`-uiG<nXG0m2mAN
zyw6!NH^7;<i6PD`|3(UDrpLJ==z>U_%0`VXaPQqBoUle4%E!jfI%hFAHOC9-__X3Z
zb(;5EW2DjxSy|05iaiut4_Gj&^O-xr+Gi0EJHh72wf~+mA2s=r8=7oA8_}1de&UfX
zjuxM&T$aWlD)i2E7M+r5XudLRkZ(oe>r5DKD6p!@F!YaYXqEZm;yRX_n!xk6sOYn#
zc7CS^=?!Bp?TEAao2Q?G*%-aM&{4#GR<U(nsFG3>2C)~jP#T?~j4mReDeii|&=_^g
zp#(oYV-+<hD6^xMqaRD-q(D_BBr-FR=1-r;x>^TQ6bH^bN!2{C*)|lB>EBSLc}l->
zA8v+h+F9b3_`qDFX6-Gx9v#S7SHY+&7aStRm{>^Q9WrbD4Ug@^Gsutd{&3Xbhkl5@
z!``Y1_NTg}dp!h9f8udo^8%;aSmY|<31Bl2#*oy%7lbd_=8)kTX+&P6QY>{N0NY2V
zowdMSmx>e_kf2G#+8W0X8087;k1esTPowh<)1K>QNLBqgKaU`)Z2Uh4h}KmZm4riK
zKtgI6$MWiMsf0|Hz+TrbtKYd*8u13q4z9i|;}5w%VE45|He0=6dqD@k;AP`~m(65K
z&Pqse^f0F!yORnt5xGp)#HblsHzqq*Tj`w&mn=?A6wd3Sl$gDy0^;vr=;gNwn{^$}
zbfwF(C9DK+2~&!AzamqK8MJCfC=X+49Db}7(a$rasC&KA*ILPt2O9p@F_QNH(kEIo
zN1#0IiH|$ep=Zd#ucv|e>%m5|v&ZRwttZ`F>A%OF{%6m5ioEaXK!_}J8O2j_S|ioG
z72lH$Km2dFak~j&{BeRCU9H9VXNgC7CLE!-DNw-E*q{$}aE7+~8y%c`7tV{y93}aH
z5OP5ZGB49gK@3$47OE_{i?x0?=rPlwBJcEkq)=;{GrdW~Fhocf@<;YbCKh^Dhhs7M
zq#l8KZPb0%xnuW^KgKlRJOesMO9)j~c+M;{?Um$1dGKLW;G4m7ye-QwaFD_X8bMPG
zhg=@=hOA>P_^m@bb}A@;Y16m`^o$xHcj|I@Cjb+jJuGPv(`k)Z_M$WQNbBKB*TSS3
z@Hh!A8CKLogXkWV6rH+8U2fjf8!>yt^ur`-!QHDc{r;S{ReFR<-$JJv2Ly`CKmzw9
zN0%n_1|EdBUJ3Pftf4%cLdm+U&Voa{UC+-73L?*sFN~JVw9hEiSKCPT-A84&;mZ2&
zQKV1R9W|@F0_ZswSisB}>NM4xfBpq#G@1Jegd^Fsn1t0QBhG)I5AL4S4p%da?B{Ss
z-)^+3cQ1`pJJ+voh1jtjX1pmf6jom6Ej^!*%FR8;4?+C>u~yh3v(oH{6mvhYtYA-!
zI6LNm0pTXcG0QPdr_a30eFfjX*`r7<V>o4yZy*)Z^kZ-+g1JD3izR_uS~?_&g!6F)
zK$?o<8X{jWmMDTBBly+awmNv&cD<S@FmQoD%$FIfzo@I(A~ZQQrp|%U;dLYcn0;l=
z>iexL+>30Nz5~A6=goyUGjg2v^zmCmjxJwtQFO`Zy4zg8zqt3=WOePQt(VDt&j5Dn
za3U~~`N$6v8|2!?ENxDTIB7V0n9qpSOUn&Qauq&V*CQ%=SDeXVr=n+EPWdvQs~Fyi
zR=GLtK>pP~g#40Wb+<;@QU=Ql8;w3z(+kd0QGF?z)(sN_Y&8CeOjR8ESxP?(|Cvi-
zU-)wZ#pW<$#S4#jd%qC@`Yhadg7JqRXT02^bu+s4>i!}{PKzB`$7KcR#mWjl?FR*k
z=*3?X2Y4s{q_3Faj9V4IOLqt%c@vedF*!(3=A@LSDPGd*r7;QVod@k_d=Tpp_YJku
zFFH?DH_=w3O%7ibe;q9~s=Qo7!6TCy7ETOsU>xFLjpJQP_IIx@EWA$VcK`NUIY}Xw
zghbj{LtulfW2J~A4-x;~V<8{nFYZ)cTgDz!60EH<=|V2HeOXm}XxIlk44E!>VO&u^
z*}6QD2Xp4=vsnERtlgKQbz(&b<7n59%CkcNgEDZ68Ej7PMy0>XtV{XtsN|IRa37Yg
zvWY7aZ4;UsjMrL(&Cz_(BIm<u_WWmZ=w(U3$<p{~%a|_F-bZ@cqJhUziSYqz5M<~8
zhSC5@wFNC;V~t)02(7i&$6p1xd-*gpP%ZHgb9*oow*B+1q>t+s)I(bm+d)CkrglfF
z1#>>cNkz(KyI=}`bLhYOExLeb=~mCk7Q1G$bA=<(07HApd<wOL*ZUcP6Xo~Ln|Zqx
z_Hk^y{5C^i*=>7<%Ke|iW&r!c<~)MLR0gCFt_$CdM5N#7O6|O*ud6etn_jm45&Mfk
zGM1?n`_&i^^0~><_hLeiT|cmq6b*1M3VUuRVzwMfpvU+gVC;lE#`(JO&!-rS+Fe@5
z-NMxUhk&{3kADP=gN>b!nT?m3lTCx2SK!~n#>vRWA;88Ktf~|Ae*_$yENm>j|9^pd
zq#faZ0@^+rI__$wUS!U$PL?+IKgis@oqv$oIJ=vJfdR7j?~veJwCL!@RK_Rd57odR
zDG^!B5iuz-6_c?bG0CI?x%w?2DH}A%##a_b1||lG24Ken<?<C+z|L^d-R~s0Q~!Me
NOkP@9s#?M%_<uVq!Cn9W

literal 0
HcmV?d00001

diff --git a/public/img/browserconfig.xml b/public/img/browserconfig.xml
new file mode 100644
index 00000000000..4473e815aef
--- /dev/null
+++ b/public/img/browserconfig.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+    <msapplication>
+        <tile>
+            <square150x150logo src="public/img/mstile-150x150.png"/>
+            <TileColor>#2b5797</TileColor>
+        </tile>
+    </msapplication>
+</browserconfig>
diff --git a/public/img/mstile-150x150.png b/public/img/mstile-150x150.png
new file mode 100644
index 0000000000000000000000000000000000000000..2360303f2ad55e968b56f19dd18ab2d06f542120
GIT binary patch
literal 9010
zcmdtIS2$eX7dJk7q6Uc)gGBE}@1!6^5WTlxL^pb8A|VMzg6Jg)L86V`8IcTP@YSL-
z$%Ih`6B7nAc;|Qdzxdz27w<mjJo`C&pS8|<*4k^Wy+3=udTjQ9h4CgM003YyHqy5Q
z0H|31_gtZ;v_v<Zexg*gK6g#;0su`}Ok@u_%KI%(BTG{NAXW?jNPZ3goKc#Rw*Y_$
z5CE{_4ghEt007(}CEXU<ln(kQCJ*!hm;bxo_f_XmS{TBOO$``+GYGJ=)2{4)T?PPd
z&lv09wT@oeTaAf#_U;@0J9$u88JDAKTHI>tc-P{Mf&SZFvy@v}A4kNfh0>kryx9#t
zadBJsGR8K}@1}0u(3i(^T)R8f8vD2V!~2&Xn^jzzRZSgx#0fn~B{Df)VoEtElyS}h
z=}0pWJW=RVMi3zshn3yX|G!j3T)Cp?y?*-#+S{frdUpmRP9w*6sv0^8B7ZH>Z!(r(
z&R_xDJH)U8j5d-+?`zlXUwuh6u6=8miHdLcq4%ekPR`Dn^@U1c5E^*=!oE+4t?24N
zM|8x@ekVcdQ#InYaI5mHxf0U>wJjAv3D{1DZ80Vmb2SDnaYdDVYQ3)R_44P9hz<MS
ziWxSD6`8byuU%bg+k$S*hJ&6`S0O%k#9$Hfjfyf50eCj<E{(e;lEg9C;2UVKs<Tfv
z*_H?yN%p-F21B0k7<voH7sDW;3o#I^+rtMPQq*@$hME(`aja<kES6=YY1^x>r`%?F
z;a|uKR_J@RG^+>Z?a>T#Pm#x6no!o8fQZzD$_Rt=XPuJ|UYkSj8$AzUMlNE^M${RZ
ze7+~>TE;U1t(*z3z26~vZhx8Q!S?y{hFFZf_CA7?=U>mK9=^_vT1)<Wb}_siWO~Rb
zhJ=D6Lszh(-#;M~UbF$b;^~nN2-Fm8ti>&c8Z%zcG8X1L@vhI>VTJb%58b*TZqC5y
zbORCR#Fux;xPXps;dSy){Fw_hc~bvCwiu?w7C;5!_PogoYitY*h*d>$lr+x*wO+KT
z$_y}Ln66_FZe7Jut5WTT2rwkk;)tDk-h&--XN%n#+)A=6W3|k#uTT9EuJkw>YpSs1
z&|7FS(7ep0C3dx{b1d|W1-jr~k(*Qgm5mPq(JBj!85K}(QPe$-E!qoevOL~uMX=Tv
z2&`IPg2(gXxiV&w@4uur5bFt%tMPS?i~CIBV1+KAcD%WRP3HGl_{=j5m=r$yDR4WB
z)h%Z9%{~{}T%D+)?S=F}Y#^X_goY~*{tD>T9%D_lUc0_gyaV;*@DH-Ly#ZHyQo_{B
zSo2cLoLA&kkniT3Pajn?RZBemZ_ex9BpN>w{Q@j~?r1Ah>;o2m;>_Gj%|igRa=1Wg
z-GA}qIMViVJVs00ri1aQ4D>gw+^hL!|8zmf9eV%8V8BQ4xQ_aA^7tLC&2jbO*u}m;
zW6!adlVOxGvYo2p?o8DSgV1IqelPl6i`W*p!1?<P>9TcXe4aBrtVUSe8<)-#McI2T
z3?hE<q*0UD6Z|Krz>D}e(iS*W4bD=><Qe$2T5oL$PM$|+iR5T^M4Qg+=cRg+3K!oY
z3K6}XlWmn5+^7=OKvs*=SGmbM>JQ(0e{Ysja;1x;M*6=h;_}H<S`=^>#+#sx(aLD_
z-_17r4l#pcww0_p-+(2aD8nj77sz!7lc7W5@e>ie<(bq4<G)`r!hR3=pPI?)cNcb-
z<ikIC%Yb7q-hEMIWds_4@qQn<GO6N>`v%u?8+faJaf()cl~d&-%D(c#;HcJlqg1On
z=wMF>k`Xr8Z&n7=`xd)7xg*wR6|l6u%!o|iJuyG7ecC1H&;EK&A89ye1p`BdufFLI
z8+9h=cb*9S1eHyztpP311jR+tWGB;8DGRiOs*jJtQp3Z?9$ozQ=>(q)J;RjQ=!sP)
zmiGIhKI1z_am}CR50^ec51V;6b-C@SacPSl>hi<tZ)zlCQFi_GorFVuBV_;^*9RjJ
z7L{Qx2mhf%X^^g9lx}D%OEk4TOC9nYva;-N+c<Xf*5O?9$3$46PF79fnf=GLa+Qqp
z;6vUO`Om%qXTAZS)Lux0T>l>fCQ$#xH(7u29kxTylAe-3!P<p5=U(vr(V!+rJDFnG
z;suX5bubmH(9o@3nX+&q!Jn@<(dtXDxee!0R?PHM9lz77h)a+HiB}oAzCW=?pH%M~
zu=7gmKE<>A#&P=S_>T_r)f?}U3DWqEd1X}{eqz@nFvSEvV`rQ14<;HaUme$<dO>hO
zkBye;n9E#zM_yG!R<wCIsNlgUInkng;qurw2VLUpHp`t2())yhy6ft%>yB!?O$G(1
zSzl_&>krK*r4U9d#RVOEV^^QM`SySEI8{tZSCA9=LNjW>hHt7A54K<FY*5<|E6AA5
zsu+ol%P9^iGf8N$pVBnxUWWAJ_N(WW8~T!huiON*W%?J#=JW}EKY#S`;c}hb1FkkM
zR{^*hvh;HFA+%y7ug+nBGg|woi>EuJ<5%x`X)Q$~KZjk!P92-U3BjhGGdH93_)gTC
zGoI~9r}?28{C({kR9s)*fmSFdn#@9_Sp6Y#dwZrAc6&Q*aS<K3C1e%XfvIa6q3o9(
zgcs<6-)CubDcV(jo-IrVn~u(?8Eu&@`+n}ClIefodPs=iY@iO8@tdz(?i1f$hmiNw
zv%EA`Kx-DZuE#~41f4~Rklf@PX9jY^x8NQD;+WsfVlkKCAKfaZu9N@|?<r@i8x1`X
zLMMz%@3h^qrjiWFy%UJ(UftIQT(3*jGcni#m$1z$tF8(XyST0PcB;ff<i53ptEjE<
z$N;&c62&>`VA+3F?hI;*W?Hg$^PL1t=d7obGE6>HLH1_!)4`VkxkKP5&ePM6_4M}1
zW?YOw(Pn>QI~KG*>^{PEraA`qUJe#MUf-Tbl5y3EIFp}MZb(4>9aBj385;A0lU=><
zj<>w{h_DbE6I}aKuznYB`;0U2-)h(iTJQwXzonssu!Obc&eLZJu+g$zDs2ij20Hhs
z>=R_$mdaJm=?G)MOQ*BLBVO9IwfOM!5=j5M_XrEWtS2j4hpz|eQ@$byQ(?Mz=YJk>
zcN!ANghg&yA*e2<W_z^LTv1MiP34>(lfuJ)|3?@c<;wYtpFK)foMwF3;vS`I&sQsQ
zaXuNNkjEk0rCT$AoME^y_W!eT2R7K(XFMR?5azGf{<B2C%d>^KcpSZDn<aK)92Di*
zBfe-w!mt?l-&||+pYKxwo2?5{^0J0*Wt~*Am70J{U+n08{+y37nsPU5Eh%Yjj=tD7
zv9B<p;Oo1j^q%tWEoj7m+(uU`znvZv?sV!5l=yOxiQkDC)g%$}Z@gtl;NvT(CUX(+
zg6iV=P|jw*#}v(O)Q#4RtomBzmd2vR?IEHDnT4n(7wf*dWSKYTRe@knX~*p5KRHg|
zQze~-Ov%JAY9xjQ2T#tW3kJx=zIaMdh<NoFJgJ7UPr(ejQd{R-&h46|apCo-GZB8~
zhaE)>j7q6hdnaf2K|M1c=^8EfAmm;HVk0T2Y0OX23jNY}`Rb8)A%qVUg`NjJ(M4P^
zq!2+VgI;L6j<r>D=kUvJJuanz-K=_+nIAt`X|)8^=inO|HNs<(14A&iWRd9fp-HF~
z(9Mc4bcAtzSHJh|ES;tQ$|D9o3Af?k?d|QZr=uz3-7dVsbC7SMgL3q`(UMa_ff7XJ
zp1#XPL6In;t{Hn<sdL9QoKQ+SbVK;C#4~Y*f6mU0Gg*}0hpIYRXC*T9+-~K3ScO<G
zNWg8t)}f16!6n-G3yV~kQLeaO^5&DgWv?pRcbz=L?kIt_B*WBk(uJBQ=Lvrs8<&qz
zS)Ns%<E~^x<9A#SIG)+_)c&y8x8>jDq#}k#sXuVAe=+jmDm$N94gbu#?>O6h@E}<%
z!*pmZs*A4p3ZVkmc3u6+f0R$Cm6BH9Y|j7U@0*SwzdrM0jz_PMJ<{~93unnz%N24x
zb8#OGQo~4Oe5*NCQQTCE8*Xpio>0ZqgCp%uTbrew%<F4`V=<j1`Bn0S$7yNGsmW`?
za_LP9d-toy;_&r1tZw%;qmH6*hadRA((Br@4#t5JABP>B@xPdGTQ1I@cxZo;%&S%$
zv+9m*wK(K!-i%t<O?di#q-5VxDd-Avere5__%(j=(`G+Q9H<`_6ZS9h{M<u5{CqV1
zh^S{M)v3<YsO(ur$FoxMA&aE8yKD)2CD&1|gyDWM;kfJ@(Q*&Ii#mbRZQ6rS+DoB7
z<O>hAa4IpMoPwYosfa-S;Ll@mFMGw~1eGxTw|)6Yrt(ZT6|@W~!Q<y~p6CXby3p=P
z4peqyIc95ve5o;7^=xymK7A|&aszpTwu&v0l#|elda0c?axmg~XqKPP+1ZAvj}WiZ
zYmvip4Hm@}#jTLFcuFN>93Q^#-WtFgQ)2ly84o4}R$v6c$*6^;{=rrRVJ>}t&Hld)
zX4F>)9y>OAmg#vXuUNS^BJg5_Sw<;O+?5jv>|4>!O4&55XM9X*n58au^F+AdQb=?!
zhL-EbQNGTr&l>Hs83`5po1Z5){+k<1C9kA%M?QIftduVRhG*9}9pj07NK%o$5r>Me
zZYrt5eNugmxt(SRf#s8lz!`vnjJ;PXVsCv%ZG)xLV$n?eK60zB3B#!+wn0weSR_Bc
zk4zsQhW4Xty^@S$wk491SH7X5**AUn>j_0)Pa4s5yNauc$Jt$*&P>{R!}0%;^>#uS
z_VtcM%;nE6kQszR)$;yXj?FNZxc))L46B!1P7hm*Y$i7F+UP2ZlqD^ASf)lqVi-XM
zOs0uen7U`){hNo9iY%Fcv{S^tHHJM`AObEYtC@68rK>2j+m49}ajqq8h=I85Wv;cR
z52E@1xRa7nIL9AdNZ_B63Qd9(S}H(tRIXXx4)wBD4(beri#|(}uAysRm(|+AV-F_F
z%8oXae#~E^V+}#hev|%!k@)<xW6n$*9&C2wSz(rO_o?TLA66udbnYjKlE@617)XYp
znqKJ}a(u&G;nEjT0k|(0`>8!ZJ$?*hhR?7ymqp+36jx{Sd0={T;#Il++VNF#HBZl(
zt6uHn4fVOL*A>q>Zle^m9kD}a7rZ>jt9p4tro^y=Bs5@^$NYwrIXJR+@Yi-QJH6tY
z@DqU(6{dk1XLy7I*IIuQ)5#<(ERW2Ev7&!KmAbgK0ctIw4q>-F9zd|Ql*2Enp6>9G
ztC}1=E2Uu^iUcX@bb9gq+eKbt_754P&e|^~*eb){C|(Irthgn(2J4V73xq0N$t8Z$
zIaxc8d9CTa`hw8Eb1CQipOMXZX=WAx>$zh1KI*9ks2LdWUgnITwVpu?){HX#DJsOW
z{uEh}V5dNLpsi*@$@)gar)^<ZBZ0_xOP`~M^X9@<mzi6T7KU!7?9a3ep1neC2QM<?
z58OQB;b&TW%dZ`D=PvsoG$$lbf96|Yo10NLncmeDjl&(xG&5j=G9JL|cAJgp|2J2I
z+zoUXrc39OS%|tCpNfz%p>R)=jV)D+Yp%W}-RuAtzA9ssz$SQB#$VtzfOg_5mo#vY
zJ&7cUp&c{g!en!D$zoTW;oJP_<@tLFE7?&!B*^|~xwn?>v=#pY_K4mvNd4f3J2%zb
z{h+zw_XiRDOarU!J}WVC5B9<X{=Ir05@gby|8J-b{WG(^Sk9h&8+A;_I!f4Jcyi2z
z4y)j|PVgTScwdo=RavB%*GN;&Ilhy3eGo^(GnY`|KoS!?mgx}H#aYIW{Iq#VcKFKW
zaq-dO5Y9?);#ym>{BP}Zmwku{!EI0vdirxV6prf%O+Do;UUR6u;5Dd{hEe;+_(VvU
z=a$pPb1lddj$UM)InSp4-iApTU9zaT`GHj`3r{*HQ*`>`$|QV$eKMMk3x<DN<-xw~
z2O8!&l;2;8(ZTCMHWH-%yYYak&BeZ0LS9;r0V55@ac5rf-nwGJccy-{96qAvXS7(J
zO>bM^Eh1}QN(Y9GIQ>i1Qb@EXWWpA>A{<XL+g%DS^LguO(q%qLkG=KQUVT<N8);GB
ze?8p@T5(6!)za77yM&XvoDEvUPUZPNGz|Y2!EoD~iT-xw`5k8ZENb|jHr|=kUh^b@
zOfjV-<$c8n^9upY;IBbd)5Ns=?OKkjJD7<}uPTJaEhP>mbI}lC<TDfzeZNuaBimOv
zCiP$TKV?!M=)+KaWL4}9)MZ(#mqoE#n8kTpyH3IS_U3Zndzo7aIRSo!mch{c@04lt
zBWMLcbKQ#ckSVf)&Plno3)GgA?Q^oIsH}1v725n^*nWL$4x%Xhwy2?c^LzA{v#~%r
zmt8L$??wlh92l(;jaS}K!4s5~E=2e1%qfcp%<O4dk*1Pv<|)BSD?Db2)pa}3jHo^n
zW;Q5ReidhC8f7%x2!a$z<@7{#aGtE<L*f7I9@6N=p+)%*8m8=Xg)GoKX)vNpks;h4
z<f^tLYdqn^s`R9=sguxL#EY5eZQ`v^8;ZMLnc{HUI;n4FHClPSS=lqj2oBmOo7;Yg
z5lDLj&q1j-qOWGzrvIAtK&3(wJI9L^mQ9N^6twrWc@-4PzAmuFWP)A19@AV8SmWTO
z1O$bh-j85OD3P;<M^QTFl6iH=M^f;xAoml^G*AgNB5m3|=zrB!YmM9!!?{^JvpFCC
zH<dV1t}gr_CCa^DHt-Z23oO{K;_ZKJ0QJm3sn1h4@@S0fZ3k-^)(QWnL`s(|FRyMU
z6pjuvK2RJig17D_7eNpxP+;VWx3>L4{|m%K>xR~E$mNKqe8-)iQ_YI1j6hHL=}r;1
zBRXCwK;NhGckm1fxjx%TNd~g?1CK}oP#T$&&J?Q;1rAxfqbom#NlB_oo8L=`l+}<Y
zvrGsVlpE&#+~2I&TMK#VO_-a*^b6t9Y#dt;2?A|-a8HaJcJT0+mWuDb18ji9VcHTs
zGPB7h^JQ3F_JAuV-pS5zH8{=w3T8kE&Tc|q8RQex51wl79OFwy1ju-grj~S2N95(O
zUspf$24*%TA;o6N+`K>fd?WD?mO&Ws;4^BVDM1TwnvqmBI&_<>vy-qy_o_3!w^41s
zu7qY6ZJYbO(*P<g&$q5ODHC#Ka7?k`Z&A?rd{cn$JdFSF<lFeXoD6~^^@8)+CCF4D
z1JQ%xh>?xAz7ExVHaXF07>HCXQ9rj3<D-LV%bW(}Ypp!{YXsQ^g)X^z48nL0G{#Af
zNRfCxg)%dO0Fz7zW|K``Tk+>JHsGCGe;oSF-2LlL%f3;>SH4Vb*Ymh&M0gt14vhrX
z-U_kBrg7zF5lc2#|GSVt9`UUDuKDOO9k-3o%WAg~x_7dpcyMH+Bj$|z!;kw7@#ktu
zUgUQpirLh|JSdVr=>RmafD$xb2X0j;-#EjAhdfA%eY$%D@#l(Kb=R<eL=F&SdZqQU
zO&zyu8I9toh6C3wP~)$iRq5L5s77EM;_BeeIQcz!j2qP{LQDFyx%-7l5%ie;^UoMy
zx1F5!OT@VQgCjj3<_Y6Dx6aRD)&`^pZdJ0H6REwbVci#|X(rfuKSAQtpAEZTs-56O
zkN7KfLZ*p1=&Y30)YB~OZS%>VeouX!uT}4CKFEmpC3_~%B<rOleGIi3Ztoq7p8MLu
zOv-z(+s7)IhRRCI4Q4yRhS=J;t~Y3AQBxeWy1{1d2z(4d%kXsNdTgQ<4%N+LphtZY
zGr2L6+mX<6tJ5hMi3y@xI&bq%oa0rCf-QUfWRcv!)RP&cAapp{hrb)8L)Cr_<6dG%
zEx#k3BZgPAPHe>pp8ok1XJf;gcal2TRbh(P?#-CVJyk`&{*l$5RDaD|gb_$L6_Ie{
ztQ5zMv;i%Q$F%Bg1tj5yk`#t$CgIqh>WRqST}?*CQ#0%^*6Ax}T8fK5vdL`rtzT~K
zXOaKzho?P8y06e|Ay27|FReGez<T@t_UPH3KY=r#c&5v}TAO)9vnIO<c`?$<p5==H
zijVmpP@zaqhi!>}u+SETpZ4n^5tAQ;<Y(}4g2!PyC6&I@38jTDOGmGEp8e~p=?-eK
zYrvh<W3U>DR7i6YDVBNi{yOH)?2UflZqJxv<P-As=;FA;oKt!hz_Zea?hfsWgq0r=
zf=td+=YQ#TOdSE;za`-N=C<<GacKt9O$Rg;Zjj2j60>gUw;t=wduXgpVhsj*uiR>0
zoXfW0l%0OKYf+jsQ9JD(`!FvXp_&E`OWZqQ3vshLeQR1gY!4$9K?E1l36g2y-bKv=
z%d6&%<d9)=Mi)b?OzH>h;R)SR_S38g+&y$Xy~*vcWTQ(m+90ZGIsaE<)Co;U<VG8R
zk~A_V@J#=O=-1YVDVpyE>;WjTn+DBvK7BoU7!jw<q-a6`cc$OD{flvl^7bF-?%5f$
zsPybU;Dg+$!2QI*Ubes`mkh-sDGEOHlu*qjLLGSx$x9#nHJU1+o<=y+(Yr_~EiPoB
z^Dw3ONqInYUALY~!t+5F`|BYD`812MW?pRPYqm(T!aiZ8n1YHRHqGUm?*x|?_6cBU
zQ-qU6S!dGmOh%gV{Mqx?&NGV?APZZ&t7Jn+c@ccGWct!st|N3iBlJa-_jeV3weJZH
zaY}SBj@^q#HeI|&1%2P(L&-F+99fPT80W^C7|15D#=6e#Fq0K*N$q&K!{a<7{M{kj
z?7u(iu93^U6k`lGREouskM`pzplw0mX$RfLXyqi!RVVKSZ#rY;ZmD&e3zf8YH|zqJ
zt`lQ6?YVr47W0$8Q0Lv(G}lP9{%%uT^jjC(_l|m)wrL~GMqar^&>eV9C#PX|Aw;q~
z<KO&7AEI>s0{U87YaW>Zt0=oPwFy?n)gs^;cNi^Mha$SdoWH*-PUI)X9MsQdc|NP@
z{6aVgTA7`{gnrn4;kBe#4^V@Pf+pbZ9f5r@gS8#;L2J*+9~VNy=J>{OCJQ6Fd~{N2
zvw>R34u#9O6>Ohc@tY(gD;uJzP1rqnqi2ai`*p*>gr}jMQ#z;hKZrqcBd>{=xd=k9
z{*_lV%t2n?-XWtaWf>SQo{@ZXMv2l5TPy6u(}VHr>S6`5ZgrXqsD5*62gj#cg9l-q
zO~dDGHu=G@z|^|^q{c7I_#k+#gmo!>Wje1hS_91*HSp>8;Y2k?6FaI8^`u@p6twXv
zUd;E{(rl{8WabY<8SY&4d;a9RFkKnwinE&xv#|>tZ)_P+4>*7F?kyUG9!f1mC(zKe
z{E2!AG#oir4@YaWUL<P~`LtDt&1R;e_O}>;od5kyYLx1D?ve#zc@sk(ed_YC#g0P^
za;b$3++W!ixwQD(=+bAPRV_cR#$?M@!W79q6l{uIa-abc4bjGM7j&SyIgNSua!c5H
zi{!xva&{@jd`FX)bzg+=H$I-qydH&$p{dXe=q%j!r?872HSRxBf6Cp;+)@|&5yi=2
z$=phgAk@0&e&W)KiP|n#<#UR%;Pq~wm*z8^p!0cKvK>@OREbn%4)WywTFIm4zQgH;
z5_v*Ly!YBa_jX#GH=+q)p>N0YBIx@rvMXdlv@|NOCcZL@B1Xqitxa0dUzUs^Lw_aS
zCix9jG0blK?0FBA3+;e5#`_?O5X=1CEX!BRL>TF3pj~(P7cLNkh`UpsRiAoTB=01~
zL3DSG-86{VO>8weO%~_{CNhD~CZEd9J$HQ`|9X0=N**-~Z;jP1g8MMQ(`&weMO3GZ
zi+tHaZL5k{CC=!WjhzQY4Mgu7xWpMWE{s<FX|Xj)>>f}McfWWt6wu0+gT;qwF6vee
zO-l(cfioV7o-36(b@LLv@lH{?mG9XF>0npo*nfOMMbrID;%PCJQ(vk~`AlLs5DXY;
zI<4{_a#d4ikq+r*O%mNcD2@BI`4Ocd)J53<Ldz`Zg0<y|2O9zF^Su4L47wGkRlB~&
z+>$OtpOAt0V>*|co*Wc+e<cSOeC+U#C}|sL_iH-g$FJ3v$=x&Gsj+*_mgs`5j0C8@
z>r7eJvB$XTrE7P1sIgi;23gRcm4rNRUc^S$^ZF}1^pFgB6b@Z#opgdt-uPYM+<3E@
zKR>CPRr#5l4ymMJI283FH<@CP_M_w1icF|1EkT9F{r!&Ye|+G9`ndZmg>OGltPW!0
z0+koWjM&NP2q?06@%GD)Wt+!*DwfPFnOUVt?CVK_Oe}Y|1z>0^5em@4E_RuWK0485
zh3(@F&5CH<dbOD9o}pjAG<+nUKC-3^?Sf+Q@rA$yx7n`vD4*JV5tEy$0kZi|s*B|U
zqat|&<eiNlexL~MP66@zUh%%FKhGL=6fD~7e3jS0ySm@t`hl}x6IhCnnpi9R)~mDU
zTy%Tm5)_bB<@q2k;luQJt6t@zVs*>U4@&!o`Ka*k3R86c9|tbI+czJLK?Ettr630m
zfPVa*-h{VA^*H>&kE3j_ElAe5ZkrP){*gOHKo@*<a_Q98%oMV>if>ur_rjh!v6Hav
z5WI6S0~55K2*t#Jd#|5xqgbhqsoVf4uaR3si<xn}dOn%;vanSBk}s-hY>T2fb+s~u
zr!;P5hcYSiM=73~C}v_V8}?59mjkNA6oM$~pg7_%FA$)#KHFFN^mo}nKwtImV|{m+
zh6+NLhjfkZt>*iiy&MJil@;58D=$;$CINoDkifS=xQBiK2=fprQD9+@0<4JahvrZH
z&(!;TSB~v6Hhd$i$ScI#ty|cBHmoYT1Cb=2lemt{X%F{`E$*rgfVlgwA6s7%_=%4?
z96Zj#Tl!B`SH^%G1s;YR_8EWO&}o^8Lc%_)_<wdly`q?pn(?OLMI%1)a18W^ze3Om
zBw*MKtx-~rS;63wT3`oqJHKa^ldPxQ!5!{0f*jPL{20X=UTkIIaKvSQ!RfodGdG6U
zZbesk@bk<`PHMu_o~kyeqIuKw&}2sPROONCq;8PY6j;_=W%oojQ%j((cO^sq-gWKd
zrg+)aIm{>pQ<+rM?f$*Mggg1WQaFfnhI_M8_8AqrA&!G;$!*co%RT}fqCe+GBQGis
zSrFK9_2_RCIANc12-#E#ws;s_g1tp?zBkX7PIF$<$Sz2sz0SCtehdmk4S<nTPC+oO
z%WS>~K+dxw2r5rJd;cJuK;(#ic1JM~+n75(eh@)$!c6r1F|z|&Dr@mc9}lXJ!IhF}
z5MvBP!?_yPq);rbkj1I&rPe^U79FePm-@_>`V|fn5RBFggb8t%v|wmCD*IEaWE*g5
zymFdLmIyH##n~%G74eQK#7W{6@#_a`t6@Ih`68bxJZ1lXpuD|`xum*sZ{qO|X9JLO
z;zKmtz&_l|BivilGt`?>0Te+B>arj;StXFQf|@4f1}RH}6g5Gh2ghKx{|`Z6u$M15
z=Kr5SwRWwALSPqbZ69vw5p^phG#Km~;C(ARCdB)eZ%DW&<q%5==8*Xc#8yJ$hxr)F
zc*7DvBg`b{$;2tlX_9lDhVzzwvht7@jc|+gt+9pKk#DGPz;6s=$wu!V$^mw`*~1U-
Tsl2270$^-lrr&VS{n`HkK!SY5

literal 0
HcmV?d00001

diff --git a/public/views/index.template.html b/public/views/index.template.html
index 4140321d633..ae35666b189 100644
--- a/public/views/index.template.html
+++ b/public/views/index.template.html
@@ -15,8 +15,11 @@
 
   <link rel="icon" type="image/png" href="public/img/fav32.png">
   <link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
-  <link rel="apple-touch-icon" href="public/img/fav32.png">
-
+  <link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png">
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="msapplication-TileColor" content="#2b5797">
+  <meta name="msapplication-config" content="public/img/browserconfig.xml">
 </head>
 
 <body ng-cloak class="theme-[[ .Theme ]]">

From de1a0e47892b950b8aceb536af66a8157bcfd3d6 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 12:51:23 +0200
Subject: [PATCH 177/380] changelog: update

[skip ci]
---
 CHANGELOG.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22c24f83b7e..1b9b98a46bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,8 +31,7 @@
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
-* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/pull/3341), thx [@mtanda](https://github.com/mtanda)
-
+* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
 
 # 5.2.2 (2018-07-25)
 

From ae0d7a3a5d051251ceba12bc656f16fbd4a9fda5 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 12:54:33 +0200
Subject: [PATCH 178/380] changelog: add notes about closing #12752

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b9b98a46bc..506042d0fb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,7 @@
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
 * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
+* **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
 
 # 5.2.2 (2018-07-25)
 

From f3d400f1a7c7b5e1c3a4f3e53219eaf5b15391c4 Mon Sep 17 00:00:00 2001
From: Alban Perillat-Merceroz <A21z@users.noreply.github.com>
Date: Fri, 25 May 2018 12:16:14 +0200
Subject: [PATCH 179/380] Add new Redshift metrics and dimensions for
 Cloudwatch datasource

 AWS/Redshift has new dimensions (`latency`, `service class`, `wmlid`) and metrics (`QueriesCompletedPerSecond`, `QueryRuntimeBreakdown`, `QueryDuration`,  `WLMQueriesCompletedPerSecond`, `WLMQueryDuration`, `WLMQueueLength`) in Cloudwatch: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/rs-metricscollected.html
---
 pkg/tsdb/cloudwatch/metric_find_query.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index 3601b73cbe2..ef1b53eaf1b 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -93,7 +93,7 @@ func init() {
 		"AWS/NATGateway":       {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
 		"AWS/NetworkELB":       {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
 		"AWS/OpsWorks":         {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
-		"AWS/Redshift":         {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+		"AWS/Redshift":         {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
 		"AWS/RDS":              {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
 		"AWS/Route53":          {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
 		"AWS/S3":               {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
@@ -144,7 +144,7 @@ func init() {
 		"AWS/NATGateway":       {"NatGatewayId"},
 		"AWS/NetworkELB":       {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
 		"AWS/OpsWorks":         {"StackId", "LayerId", "InstanceId"},
-		"AWS/Redshift":         {"NodeID", "ClusterIdentifier"},
+		"AWS/Redshift":         {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"},
 		"AWS/RDS":              {"DBInstanceIdentifier", "DBClusterIdentifier", "DbClusterIdentifier", "DatabaseClass", "EngineName", "Role"},
 		"AWS/Route53":          {"HealthCheckId", "Region"},
 		"AWS/S3":               {"BucketName", "StorageType", "FilterId"},

From 76f131aa80d0c6149573ceb447dd6331bc7b0fbb Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 15:09:23 +0200
Subject: [PATCH 180/380] changelog: add notes about closing #12063

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 506042d0fb5..e6a116be927 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@
 * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88)
 * **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow)
+* **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)

From ff0ca6b7e214d0ca9b107058f2942ed289b5e615 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Thu, 2 Aug 2018 15:25:48 +0200
Subject: [PATCH 181/380] added two new classes for color, fixed so link has
 value color

---
 public/app/plugins/panel/table/renderer.ts | 20 ++++++++++++--------
 public/sass/components/_panel_table.scss   | 20 ++++++++++++++------
 2 files changed, 26 insertions(+), 14 deletions(-)

diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts
index e4d3626c3b9..95f54a64904 100644
--- a/public/app/plugins/panel/table/renderer.ts
+++ b/public/app/plugins/panel/table/renderer.ts
@@ -216,8 +216,8 @@ export class TableRenderer {
     var cellClass = '';
 
     if (this.colorState.cell) {
-      style = ' style="background-color:' + this.colorState.cell + ';color: white"';
-      cellClasses.push('white');
+      style = ' style="background-color:' + this.colorState.cell + '"';
+      cellClasses.push('table-panel-color-cell');
       this.colorState.cell = null;
     } else if (this.colorState.value) {
       style = ' style="color:' + this.colorState.value + '"';
@@ -253,11 +253,8 @@ export class TableRenderer {
 
       cellClasses.push('table-panel-cell-link');
 
-      if (this.colorState.row) {
-        cellClasses.push('white');
-      }
       columnHtml += `
-        <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right">
+        <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${style}>
           ${value}
         </a>
       `;
@@ -291,6 +288,8 @@ export class TableRenderer {
     let startPos = page * pageSize;
     let endPos = Math.min(startPos + pageSize, this.table.rows.length);
     var html = '';
+    let rowClasses = [];
+    let rowClass = '';
 
     for (var y = startPos; y < endPos; y++) {
       let row = this.table.rows[y];
@@ -301,11 +300,16 @@ export class TableRenderer {
       }
 
       if (this.colorState.row) {
-        rowStyle = ' style="background-color:' + this.colorState.row + ';color: white"';
+        rowStyle = ' style="background-color:' + this.colorState.row + '"';
+        rowClasses.push('table-panel-color-row');
         this.colorState.row = null;
       }
 
-      html += '<tr ' + rowStyle + '>' + cellHtml + '</tr>';
+      if (rowClasses.length) {
+        rowClass = ' class="' + rowClasses.join(' ') + '"';
+      }
+
+      html += '<tr ' + rowClass + rowStyle + '>' + cellHtml + '</tr>';
     }
 
     return html;
diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss
index fc14236c2b7..225238b102c 100644
--- a/public/sass/components/_panel_table.scss
+++ b/public/sass/components/_panel_table.scss
@@ -87,12 +87,6 @@
         height: 100%;
         display: inline-block;
       }
-
-      &.white {
-        a {
-          color: white;
-        }
-      }
     }
 
     &.cell-highlighted:hover {
@@ -139,3 +133,17 @@
   height: 0px;
   line-height: 0px;
 }
+
+.table-panel-color-cell {
+  color: white;
+  a {
+    color: white;
+  }
+}
+
+.table-panel-color-row {
+  color: white;
+  a {
+    color: white;
+  }
+}

From 4962bf9d44ece9c08d28a25250d8f4125facb579 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 15:49:50 +0200
Subject: [PATCH 182/380] remove info logging

---
 pkg/tsdb/sql_engine.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index 29428971c64..3f681a5cdd7 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -253,7 +253,6 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 				columnType := columnTypes[i].DatabaseTypeName()
 
 				for _, mct := range e.metricColumnTypes {
-					e.log.Info(mct)
 					if columnType == mct {
 						metricIndex = i
 						continue

From 7f4f130a803a80e15f2d34306904632b2ff142ed Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 16:21:49 +0200
Subject: [PATCH 183/380] adjust test dashboards

---
 .../datasource_tests_mssql_unittest.json         | 16 ++++++++--------
 .../datasource_tests_mysql_unittest.json         | 16 ++++++++--------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
index 0c7cc0fcc65..80d3e1a5889 100644
--- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
@@ -369,7 +369,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -452,7 +452,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -535,7 +535,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -618,7 +618,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -701,7 +701,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -784,7 +784,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -871,7 +871,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement as metric, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroupAlias(time, '$summarize'), \n  measurement as metric, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
           "refId": "A"
         }
       ],
@@ -968,7 +968,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  avg(valueOne) as valueOne, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize')\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroupAlias(time, '$summarize'), \n  avg(valueOne) as valueOne, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize')\nORDER BY 1",
           "refId": "A"
         },
         {
diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
index e95eedf254c..f684186084a 100644
--- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
@@ -369,7 +369,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -452,7 +452,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -535,7 +535,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -618,7 +618,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -701,7 +701,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -784,7 +784,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
           "refId": "A"
         }
       ],
@@ -871,7 +871,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement as metric, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroupAlias(time, '$summarize'), \n  measurement as metric, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1",
           "refId": "A"
         }
       ],
@@ -968,7 +968,7 @@
         {
           "alias": "",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  avg(valueOne) as valueOne, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  measurement IN($metric)\nGROUP BY 1\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroupAlias(time, '$summarize'), \n  avg(valueOne) as valueOne, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  measurement IN($metric)\nGROUP BY 1\nORDER BY 1",
           "refId": "A"
         }
       ],

From e5178b7d1d128a373ff33c304a3c26d0f5d77acc Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 16:34:16 +0200
Subject: [PATCH 184/380] changelog: add notes about closing #12766

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6a116be927..6f7be5caae2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@
 * **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow)
 * **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
+* **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)

From 7f85dd055ebc67f7c4855179fa819598170adb90 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 16:44:29 +0200
Subject: [PATCH 185/380] changelog: add notes about closing #12749

[skip ci]
---
 CHANGELOG.md | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f7be5caae2..f699898e5f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)
+* **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
@@ -36,6 +37,10 @@
 * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
 * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
 
+### Breaking changes
+
+* Postgres datasource no longer automatically adds time column alias when using the $__timeGroup alias. However, there's code in place which should make this change backward compatible and shouldn't create any issues.
+
 # 5.2.2 (2018-07-25)
 
 ### Minor

From cb76fc7f2d307faa9a530cc3cb66deee8aa31682 Mon Sep 17 00:00:00 2001
From: gzzo <guidorainuzzo@gmail.com>
Date: Thu, 2 Aug 2018 12:29:47 -0400
Subject: [PATCH 186/380] Add auto_assign_org_id to defaults.ini

For #12801
---
 conf/defaults.ini | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/conf/defaults.ini b/conf/defaults.ini
index 6c27886c649..b0caed81e90 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -213,6 +213,9 @@ allow_org_create = false
 # Set to true to automatically assign new users to the default organization (id 1)
 auto_assign_org = true
 
+# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true)
+auto_assign_org_id = 1
+
 # Default role new users will be automatically assigned (if auto_assign_org above is set to true)
 auto_assign_org_role = Viewer
 

From 72af8a70440761a470a9804fea5b367b0aff953c Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 2 Aug 2018 18:57:13 +0200
Subject: [PATCH 187/380] changelog: add notes about closing #1823 #12801

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f699898e5f4..5298dcd04f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
 * **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
 * **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
+* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
 
 ### Minor
 

From 62d3655da43d712e32c1cb2f1a406c157e477478 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Thu, 26 Jul 2018 13:40:52 +0200
Subject: [PATCH 188/380] docker: inital copy of the grafana-docker files.

---
 .circleci/config.yml                   | 28 ++++++++-
 packaging/docker/Dockerfile            | 38 ++++++++++++
 packaging/docker/build.sh              | 22 +++++++
 packaging/docker/push_to_docker_hub.sh | 17 ++++++
 packaging/docker/run.sh                | 82 ++++++++++++++++++++++++++
 5 files changed, 184 insertions(+), 3 deletions(-)
 create mode 100644 packaging/docker/Dockerfile
 create mode 100755 packaging/docker/build.sh
 create mode 100755 packaging/docker/push_to_docker_hub.sh
 create mode 100755 packaging/docker/run.sh

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 44f34d42926..01cd36261fc 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -89,7 +89,7 @@ jobs:
           name: run linters
           command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
       - run:
-          name: run go vet 
+          name: run go vet
           command: 'go vet ./pkg/...'
 
   test-frontend:
@@ -159,6 +159,16 @@ jobs:
       - store_artifacts:
           path: dist
 
+    build-deploy-docker-master:
+      docker:
+        - image: docker:stable-git
+      steps:
+        - checkout
+        - setup_remote_docker
+        - run: docker info
+        - run: echo $GRAFANA_VERSION
+        - run: ./build.sh ${GRAFANA_VERSION}
+
   build-enterprise:
     docker:
      - image: grafana/build-container:v0.1
@@ -246,7 +256,7 @@ workflows:
   test-and-build:
     jobs:
       - build-all:
-          filters: *filter-only-master
+          filters: *filter-not-release
       - build-enterprise:
           filters: *filter-only-master
       - codespell:
@@ -270,7 +280,19 @@ workflows:
             - gometalinter
             - mysql-integration-test
             - postgres-integration-test
-          filters: *filter-only-master           
+          filters: *filter-only-master
+      - build-deploy-docker-master:
+          requires:
+            - build-all
+            - test-backend
+            - test-frontend
+            - codespell
+            - gometalinter
+            - mysql-integration-test
+            - postgres-integration-test
+          filters:
+            branches:
+              only: grafana-docker
       - deploy-enterprise-master:
           requires:
             - build-all
diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile
new file mode 100644
index 00000000000..6e4a5896b75
--- /dev/null
+++ b/packaging/docker/Dockerfile
@@ -0,0 +1,38 @@
+FROM debian:stretch-slim
+
+ARG GRAFANA_URL="https://s3-us-west-2.amazonaws.com/grafana-releases/master/grafana-latest.linux-x64.tar.gz"
+ARG GF_UID="472"
+ARG GF_GID="472"
+
+ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
+    GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
+    GF_PATHS_DATA="/var/lib/grafana" \
+    GF_PATHS_HOME="/usr/share/grafana" \
+    GF_PATHS_LOGS="/var/log/grafana" \
+    GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
+    GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
+
+RUN apt-get update && apt-get install -qq -y tar libfontconfig curl ca-certificates && \
+    mkdir -p "$GF_PATHS_HOME/.aws" && \
+    curl "$GRAFANA_URL" | tar xfvz - --strip-components=1 -C "$GF_PATHS_HOME" && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/* && \
+    groupadd -r -g $GF_GID grafana && \
+    useradd -r -u $GF_UID -g grafana grafana && \
+    mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
+             "$GF_PATHS_PROVISIONING/dashboards" \
+             "$GF_PATHS_LOGS" \
+             "$GF_PATHS_PLUGINS" \
+             "$GF_PATHS_DATA" && \
+    cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
+    cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
+    chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
+    chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
+
+EXPOSE 3000
+
+COPY ./run.sh /run.sh
+
+USER grafana
+WORKDIR /
+ENTRYPOINT [ "/run.sh" ]
\ No newline at end of file
diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh
new file mode 100755
index 00000000000..ac1dd41feec
--- /dev/null
+++ b/packaging/docker/build.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+_grafana_tag=$1
+_grafana_version=$(echo ${_grafana_tag} | cut -d "v" -f 2)
+_docker_repo=${2:-grafana/grafana}
+
+
+echo ${_grafana_version}
+
+if [ "$_grafana_version" != "" ]; then
+	echo "Building version ${_grafana_version}"
+	docker build \
+		--build-arg GRAFANA_URL="https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${_grafana_version}.linux-amd64.tar.gz" \
+		--tag "${_docker_repo}:${_grafana_version}" \
+		--no-cache=true .
+	docker tag ${_docker_repo}:${_grafana_version} ${_docker_repo}:latest
+else
+	echo "Building latest for master"
+	docker build \
+		--tag "grafana/grafana:master" \
+		.
+fi
diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh
new file mode 100755
index 00000000000..4b23996f67f
--- /dev/null
+++ b/packaging/docker/push_to_docker_hub.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+_grafana_tag=$1
+_grafana_version=$(echo ${_grafana_tag} | cut -d "v" -f 2)
+
+if [ "$_grafana_version" != "" ]; then
+	echo "pushing grafana/grafana:${_grafana_version}"
+	docker push grafana/grafana:${_grafana_version}
+
+	if echo "$_grafana_version" | grep -viqF beta; then
+		echo "pushing grafana/grafana:latest"
+		docker push grafana/grafana:latest
+	fi
+else
+	echo "pushing grafana/grafana:master"
+	docker push grafana/grafana:master
+fi
diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh
new file mode 100755
index 00000000000..44411f0f6b6
--- /dev/null
+++ b/packaging/docker/run.sh
@@ -0,0 +1,82 @@
+#!/bin/bash -e
+
+PERMISSIONS_OK=0
+
+if [ ! -r "$GF_PATHS_CONFIG" ]; then
+    echo "GF_PATHS_CONFIG='$GF_PATHS_CONFIG' is not readable."
+    PERMISSIONS_OK=1
+fi
+
+if [ ! -w "$GF_PATHS_DATA" ]; then
+    echo "GF_PATHS_DATA='$GF_PATHS_DATA' is not writable."
+    PERMISSIONS_OK=1
+fi
+
+if [ ! -r "$GF_PATHS_HOME" ]; then
+    echo "GF_PATHS_HOME='$GF_PATHS_HOME' is not readable."
+    PERMISSIONS_OK=1
+fi
+
+if [ $PERMISSIONS_OK -eq 1 ]; then
+    echo "You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later"
+fi
+
+if [ ! -d "$GF_PATHS_PLUGINS" ]; then
+    mkdir "$GF_PATHS_PLUGINS"
+fi
+
+if [ ! -z ${GF_AWS_PROFILES+x} ]; then
+    > "$GF_PATHS_HOME/.aws/credentials"
+
+    for profile in ${GF_AWS_PROFILES}; do
+        access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID"
+        secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY"
+        region_varname="GF_AWS_${profile}_REGION"
+
+        if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then
+            echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials"
+            echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            if [ ! -z "${!region_varname}" ]; then
+                echo "region = ${!region_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            fi
+        fi
+    done
+
+    chmod 600 "$GF_PATHS_HOME/.aws/credentials"
+fi
+
+# Convert all environment variables with names ending in _FILE into the content of
+# the file that they point at and use the name without the trailing _FILE.
+# This can be used to carry in Docker secrets.
+for VAR_NAME in $(env | grep '^GF_[^=]\+_FILE=.\+' | sed -r "s/([^=]*)_FILE=.*/\1/g"); do
+    VAR_NAME_FILE="$VAR_NAME"_FILE
+    if [ "${!VAR_NAME}" ]; then
+        echo >&2 "ERROR: Both $VAR_NAME and $VAR_NAME_FILE are set (but are exclusive)"
+        exit 1
+    fi
+    echo "Getting secret $VAR_NAME from ${!VAR_NAME_FILE}"
+    export "$VAR_NAME"="$(< "${!VAR_NAME_FILE}")"
+    unset "$VAR_NAME_FILE"
+done
+
+export HOME="$GF_PATHS_HOME"
+
+if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then
+  OLDIFS=$IFS
+  IFS=','
+  for plugin in ${GF_INSTALL_PLUGINS}; do
+    IFS=$OLDIFS
+    grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}
+  done
+fi
+
+exec grafana-server                                         \
+  --homepath="$GF_PATHS_HOME"                               \
+  --config="$GF_PATHS_CONFIG"                               \
+  "$@"                                                      \
+  cfg:default.log.mode="console"                            \
+  cfg:default.paths.data="$GF_PATHS_DATA"                   \
+  cfg:default.paths.logs="$GF_PATHS_LOGS"                   \
+  cfg:default.paths.plugins="$GF_PATHS_PLUGINS"             \
+  cfg:default.paths.provisioning="$GF_PATHS_PROVISIONING"

From bfe41d3cf15654f86e7c879b8e927f4daeaacff5 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Thu, 26 Jul 2018 16:46:36 +0200
Subject: [PATCH 189/380] build: new workflow for PR:s and branches.

---
 .circleci/config.yml   | 104 ++++++++++++++++++++++++++++-------------
 scripts/build/build.sh |  22 ++++-----
 2 files changed, 81 insertions(+), 45 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 01cd36261fc..6dc3cdf378b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -5,9 +5,11 @@ aliases:
       ignore: /.*/
     tags:
       only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
-  - &filter-not-release
+  - &filter-not-release-or-master
     tags:
       ignore: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
+    branches:
+      ignore: master
   - &filter-only-master
     branches:
       only: master
@@ -156,18 +158,39 @@ jobs:
             - dist/grafana*
             - scripts/*.sh
             - scripts/publish
-      - store_artifacts:
-          path: dist
 
-    build-deploy-docker-master:
-      docker:
-        - image: docker:stable-git
-      steps:
-        - checkout
-        - setup_remote_docker
-        - run: docker info
-        - run: echo $GRAFANA_VERSION
-        - run: ./build.sh ${GRAFANA_VERSION}
+  build:
+    docker:
+     - image: grafana/build-container:1.0.0
+    working_directory: /go/src/github.com/grafana/grafana
+    steps:
+      - checkout
+      - run:
+          name: prepare build tools
+          command: '/tmp/bootstrap.sh'
+      - run:
+          name: build and package grafana
+          command: './scripts/build/build.sh'
+      - run:
+          name: sign packages
+          command: './scripts/build/sign_packages.sh'
+      - run:
+          name: sha-sum packages
+          command: 'go run build.go sha-dist'
+      - persist_to_workspace:
+          root: .
+          paths:
+            - dist/grafana*
+
+  build-docker:
+    docker:
+      - image: docker:stable-git
+    steps:
+      - checkout
+      - setup_remote_docker
+      - run: docker info
+      - run: echo $GRAFANA_VERSION
+      - run: cd packaging/docker && ./build.sh ${GRAFANA_VERSION}
 
   build-enterprise:
     docker:
@@ -253,24 +276,24 @@ jobs:
 
 workflows:
   version: 2
-  test-and-build:
+  build-master:
     jobs:
       - build-all:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - build-enterprise:
           filters: *filter-only-master
       - codespell:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - gometalinter:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - test-frontend:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - test-backend:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - mysql-integration-test:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - postgres-integration-test:
-          filters: *filter-not-release
+          filters: *filter-only-master
       - deploy-master:
           requires:
             - build-all
@@ -281,18 +304,6 @@ workflows:
             - mysql-integration-test
             - postgres-integration-test
           filters: *filter-only-master
-      - build-deploy-docker-master:
-          requires:
-            - build-all
-            - test-backend
-            - test-frontend
-            - codespell
-            - gometalinter
-            - mysql-integration-test
-            - postgres-integration-test
-          filters:
-            branches:
-              only: grafana-docker
       - deploy-enterprise-master:
           requires:
             - build-all
@@ -331,3 +342,32 @@ workflows:
             - mysql-integration-test
             - postgres-integration-test
           filters: *filter-only-release
+
+  build-branches-and-prs:
+      jobs:
+        - build:
+            filters: *filter-not-release-or-master
+        - codespell:
+            filters: *filter-not-release-or-master
+        - gometalinter:
+            filters: *filter-not-release-or-master
+        - test-frontend:
+            filters: *filter-not-release-or-master
+        - test-backend:
+            filters: *filter-not-release-or-master
+        - mysql-integration-test:
+            filters: *filter-not-release-or-master
+        - postgres-integration-test:
+            filters: *filter-not-release-or-master
+        - build-docker:
+            requires:
+              - build
+              - test-backend
+              - test-frontend
+              - codespell
+              - gometalinter
+              - mysql-integration-test
+              - postgres-integration-test
+            filters:
+              branches:
+                only: grafana-docker
diff --git a/scripts/build/build.sh b/scripts/build/build.sh
index cee80822cac..a02f079dd72 100755
--- a/scripts/build/build.sh
+++ b/scripts/build/build.sh
@@ -14,12 +14,14 @@ echo "current dir: $(pwd)"
 
 if [ "$CIRCLE_TAG" != "" ]; then
   echo "Building releases from tag $CIRCLE_TAG"
-  CC=${CCX64} go run build.go -includeBuildNumber=false build
+  OPT="-includeBuildNumber=false"
 else
   echo "Building incremental build for $CIRCLE_BRANCH"
-  CC=${CCX64} go run build.go -buildNumber=${CIRCLE_BUILD_NUM} build
+  OPT="-buildNumber=${CIRCLE_BUILD_NUM}"
 fi
 
+CC=${CCX64} go run build.go ${OPT} build
+
 yarn install --pure-lockfile --no-progress
 
 echo "current dir: $(pwd)"
@@ -28,14 +30,8 @@ if [ -d "dist" ]; then
   rm -rf dist
 fi
 
-if [ "$CIRCLE_TAG" != "" ]; then
-  echo "Building frontend from tag $CIRCLE_TAG"
-  go run build.go -includeBuildNumber=false build-frontend
-  echo "Packaging a release from tag $CIRCLE_TAG"
-  go run build.go -goos linux -pkg-arch amd64 -includeBuildNumber=false package-only latest
-else
-  echo "Building frontend for $CIRCLE_BRANCH"
-  go run build.go -buildNumber=${CIRCLE_BUILD_NUM} build-frontend
-  echo "Packaging incremental build for $CIRCLE_BRANCH"
-  go run build.go -goos linux -pkg-arch amd64 -buildNumber=${CIRCLE_BUILD_NUM} package-only latest
-fi
+echo "Building frontend"
+go run build.go ${OPT} build-frontend
+
+echo "Packaging"
+go run build.go -goos linux -pkg-arch amd64 ${OPT} package-only latest

From e3a907214d822fd4db0f89cf1489165b742ec7ad Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Sat, 28 Jul 2018 23:00:59 +0200
Subject: [PATCH 190/380] build: builds docker image from local grafna tgz.

---
 .circleci/config.yml        |  1 +
 packaging/docker/Dockerfile | 11 +++++++----
 packaging/docker/build.sh   |  1 -
 3 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6dc3cdf378b..74c4ee6c3ef 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -190,6 +190,7 @@ jobs:
       - setup_remote_docker
       - run: docker info
       - run: echo $GRAFANA_VERSION
+      - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
       - run: cd packaging/docker && ./build.sh ${GRAFANA_VERSION}
 
   build-enterprise:
diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile
index 6e4a5896b75..3025b03f920 100644
--- a/packaging/docker/Dockerfile
+++ b/packaging/docker/Dockerfile
@@ -1,6 +1,6 @@
 FROM debian:stretch-slim
 
-ARG GRAFANA_URL="https://s3-us-west-2.amazonaws.com/grafana-releases/master/grafana-latest.linux-x64.tar.gz"
+ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz"
 ARG GF_UID="472"
 ARG GF_GID="472"
 
@@ -12,9 +12,12 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
     GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
     GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
 
-RUN apt-get update && apt-get install -qq -y tar libfontconfig curl ca-certificates && \
+COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
+
+RUN apt-get update && apt-get install -qq -y tar libfontconfig ca-certificates && \
     mkdir -p "$GF_PATHS_HOME/.aws" && \
-    curl "$GRAFANA_URL" | tar xfvz - --strip-components=1 -C "$GF_PATHS_HOME" && \
+    tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C "$GF_PATHS_HOME" && \
+    rm /tmp/grafana.tar.gz && \
     apt-get autoremove -y && \
     rm -rf /var/lib/apt/lists/* && \
     groupadd -r -g $GF_GID grafana && \
@@ -35,4 +38,4 @@ COPY ./run.sh /run.sh
 
 USER grafana
 WORKDIR /
-ENTRYPOINT [ "/run.sh" ]
\ No newline at end of file
+ENTRYPOINT [ "/run.sh" ]
diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh
index ac1dd41feec..df0a809c754 100755
--- a/packaging/docker/build.sh
+++ b/packaging/docker/build.sh
@@ -10,7 +10,6 @@ echo ${_grafana_version}
 if [ "$_grafana_version" != "" ]; then
 	echo "Building version ${_grafana_version}"
 	docker build \
-		--build-arg GRAFANA_URL="https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${_grafana_version}.linux-amd64.tar.gz" \
 		--tag "${_docker_repo}:${_grafana_version}" \
 		--no-cache=true .
 	docker tag ${_docker_repo}:${_grafana_version} ${_docker_repo}:latest

From e8489304760d781498d49b31bbfb90515382c9f8 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Sun, 29 Jul 2018 12:04:31 +0200
Subject: [PATCH 191/380] build: attach built resources.

---
 .circleci/config.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 74c4ee6c3ef..5fe09fbb349 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -187,6 +187,8 @@ jobs:
       - image: docker:stable-git
     steps:
       - checkout
+      - attach_workspace:
+          at: .
       - setup_remote_docker
       - run: docker info
       - run: echo $GRAFANA_VERSION

From 580e2c36d1575d205aad8b3c33f3d1b8b90a9b41 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 30 Jul 2018 14:05:56 +0200
Subject: [PATCH 192/380] build: imported latest changes from grafana-docker.

---
 .circleci/config.yml                   |  7 +++--
 packaging/docker/build-deploy.sh       | 13 +++++++++
 packaging/docker/build.sh              | 37 +++++++++++++++-----------
 packaging/docker/deploy_to_k8s.sh      |  6 +++++
 packaging/docker/push_to_docker_hub.sh | 22 +++++++++------
 packaging/docker/run.sh                |  8 +++---
 6 files changed, 62 insertions(+), 31 deletions(-)
 create mode 100755 packaging/docker/build-deploy.sh
 create mode 100755 packaging/docker/deploy_to_k8s.sh

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5fe09fbb349..d59e4984454 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -182,7 +182,7 @@ jobs:
           paths:
             - dist/grafana*
 
-  build-docker:
+  grafana-docker-master:
     docker:
       - image: docker:stable-git
     steps:
@@ -191,9 +191,8 @@ jobs:
           at: .
       - setup_remote_docker
       - run: docker info
-      - run: echo $GRAFANA_VERSION
       - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
-      - run: cd packaging/docker && ./build.sh ${GRAFANA_VERSION}
+      - run: cd packaging/docker && ./build-deploy.sh "grafana-docker-${CIRCLE_SHA1}"
 
   build-enterprise:
     docker:
@@ -362,7 +361,7 @@ workflows:
             filters: *filter-not-release-or-master
         - postgres-integration-test:
             filters: *filter-not-release-or-master
-        - build-docker:
+        - grafana-docker-master:
             requires:
               - build
               - test-backend
diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh
new file mode 100755
index 00000000000..923b1b8f3c0
--- /dev/null
+++ b/packaging/docker/build-deploy.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+_grafana_version=$1
+./build.sh "$_grafana_version"
+docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
+
+#./push_to_docker_hub.sh "$_grafana_version"
+echo "Would have deployed $_grafana_version"
+
+if echo "$_grafana_version" | grep -q "^master-"; then
+  apk add --no-cache curl
+  ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
+fi
diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh
index df0a809c754..579d65eebb3 100755
--- a/packaging/docker/build.sh
+++ b/packaging/docker/build.sh
@@ -1,21 +1,28 @@
 #!/bin/sh
 
 _grafana_tag=$1
-_grafana_version=$(echo ${_grafana_tag} | cut -d "v" -f 2)
-_docker_repo=${2:-grafana/grafana}
 
-
-echo ${_grafana_version}
-
-if [ "$_grafana_version" != "" ]; then
-	echo "Building version ${_grafana_version}"
-	docker build \
-		--tag "${_docker_repo}:${_grafana_version}" \
-		--no-cache=true .
-	docker tag ${_docker_repo}:${_grafana_version} ${_docker_repo}:latest
+# If the tag starts with v, treat this as a official release
+if echo "$_grafana_tag" | grep -q "^v"; then
+	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
+	_grafana_url="https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${_grafana_version}.linux-amd64.tar.gz"
+	_docker_repo=${2:-grafana/grafana}
 else
-	echo "Building latest for master"
-	docker build \
-		--tag "grafana/grafana:master" \
-		.
+	_grafana_version=$_grafana_tag
+	_grafana_url="https://s3-us-west-2.amazonaws.com/grafana-releases/master/grafana-${_grafana_version}.linux-x64.tar.gz"
+	_docker_repo=${2:-grafana/grafana-dev}
+fi
+
+echo "Building ${_docker_repo}:${_grafana_version} from ${_grafana_url}"
+
+docker build \
+	--build-arg GRAFANA_URL="${_grafana_url}" \
+	--tag "${_docker_repo}:${_grafana_version}" \
+	--no-cache=true .
+
+# Tag as 'latest' for official release; otherwise tag as grafana/grafana:master
+if echo "$_grafana_tag" | grep -q "^v"; then
+	docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest"
+else
+	docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master"
 fi
diff --git a/packaging/docker/deploy_to_k8s.sh b/packaging/docker/deploy_to_k8s.sh
new file mode 100755
index 00000000000..26cf88ef688
--- /dev/null
+++ b/packaging/docker/deploy_to_k8s.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+curl -s --header "Content-Type: application/json" \
+     --data "{\"build_parameters\": {\"CIRCLE_JOB\": \"deploy\", \"IMAGE_NAMES\": \"$1\"}}" \
+     --request POST \
+     https://circleci.com/api/v1.1/project/github/raintank/deployment_tools/tree/master?circle-token=$CIRCLE_TOKEN
diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh
index 4b23996f67f..e779b04d68d 100755
--- a/packaging/docker/push_to_docker_hub.sh
+++ b/packaging/docker/push_to_docker_hub.sh
@@ -1,16 +1,22 @@
 #!/bin/sh
 
 _grafana_tag=$1
-_grafana_version=$(echo ${_grafana_tag} | cut -d "v" -f 2)
 
-if [ "$_grafana_version" != "" ]; then
-	echo "pushing grafana/grafana:${_grafana_version}"
-	docker push grafana/grafana:${_grafana_version}
+# If the tag starts with v, treat this as a official release
+if echo "$_grafana_tag" | grep -q "^v"; then
+	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
+	_docker_repo=${2:-grafana/grafana}
+else
+	_grafana_version=$_grafana_tag
+	_docker_repo=${2:-grafana/grafana-dev}
+fi
 
-	if echo "$_grafana_version" | grep -viqF beta; then
-		echo "pushing grafana/grafana:latest"
-		docker push grafana/grafana:latest
-	fi
+echo "pushing ${_docker_repo}:${_grafana_version}"
+docker push "${_docker_repo}:${_grafana_version}"
+
+if echo "$_grafana_tag" | grep -q "^v"; then
+	echo "pushing ${_docker_repo}:latest"
+	docker push "${_docker_repo}:latest"
 else
 	echo "pushing grafana/grafana:master"
 	docker push grafana/grafana:master
diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh
index 44411f0f6b6..2d2318a9210 100755
--- a/packaging/docker/run.sh
+++ b/packaging/docker/run.sh
@@ -46,11 +46,11 @@ if [ ! -z ${GF_AWS_PROFILES+x} ]; then
     chmod 600 "$GF_PATHS_HOME/.aws/credentials"
 fi
 
-# Convert all environment variables with names ending in _FILE into the content of
-# the file that they point at and use the name without the trailing _FILE.
+# Convert all environment variables with names ending in __FILE into the content of
+# the file that they point at and use the name without the trailing __FILE.
 # This can be used to carry in Docker secrets.
-for VAR_NAME in $(env | grep '^GF_[^=]\+_FILE=.\+' | sed -r "s/([^=]*)_FILE=.*/\1/g"); do
-    VAR_NAME_FILE="$VAR_NAME"_FILE
+for VAR_NAME in $(env | grep '^GF_[^=]\+__FILE=.\+' | sed -r "s/([^=]*)__FILE=.*/\1/g"); do
+    VAR_NAME_FILE="$VAR_NAME"__FILE
     if [ "${!VAR_NAME}" ]; then
         echo >&2 "ERROR: Both $VAR_NAME and $VAR_NAME_FILE are set (but are exclusive)"
         exit 1

From 424aa6e564fc6419c9192a4ee6cf74550f5aad67 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 30 Jul 2018 16:35:30 +0200
Subject: [PATCH 193/380] build: removes unused args to docker build.

---
 packaging/docker/build.sh | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh
index 579d65eebb3..c303c71cd5f 100755
--- a/packaging/docker/build.sh
+++ b/packaging/docker/build.sh
@@ -5,18 +5,15 @@ _grafana_tag=$1
 # If the tag starts with v, treat this as a official release
 if echo "$_grafana_tag" | grep -q "^v"; then
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
-	_grafana_url="https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${_grafana_version}.linux-amd64.tar.gz"
 	_docker_repo=${2:-grafana/grafana}
 else
 	_grafana_version=$_grafana_tag
-	_grafana_url="https://s3-us-west-2.amazonaws.com/grafana-releases/master/grafana-${_grafana_version}.linux-x64.tar.gz"
 	_docker_repo=${2:-grafana/grafana-dev}
 fi
 
-echo "Building ${_docker_repo}:${_grafana_version} from ${_grafana_url}"
+echo "Building ${_docker_repo}:${_grafana_version}"
 
 docker build \
-	--build-arg GRAFANA_URL="${_grafana_url}" \
 	--tag "${_docker_repo}:${_grafana_version}" \
 	--no-cache=true .
 

From 99a9dbb04f161eac59cc7450c0daf5934a70129a Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 30 Jul 2018 18:52:49 +0200
Subject: [PATCH 194/380] build: complete docker build for master and releases.

---
 .circleci/config.yml               | 46 +++++++++++++++++++++---------
 packaging/docker/README.md         | 45 +++++++++++++++++++++++++++++
 packaging/docker/build-deploy.sh   |  3 +-
 packaging/docker/custom/Dockerfile | 16 +++++++++++
 4 files changed, 95 insertions(+), 15 deletions(-)
 create mode 100644 packaging/docker/README.md
 create mode 100644 packaging/docker/custom/Dockerfile

diff --git a/.circleci/config.yml b/.circleci/config.yml
index d59e4984454..818f30f7eea 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -192,7 +192,19 @@ jobs:
       - setup_remote_docker
       - run: docker info
       - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
-      - run: cd packaging/docker && ./build-deploy.sh "grafana-docker-${CIRCLE_SHA1}"
+      - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
+
+  grafana-docker-release:
+      docker:
+        - image: docker:stable-git
+      steps:
+        - checkout
+        - attach_workspace:
+            at: .
+        - setup_remote_docker
+        - run: docker info
+        - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+        - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
 
   build-enterprise:
     docker:
@@ -306,6 +318,16 @@ workflows:
             - mysql-integration-test
             - postgres-integration-test
           filters: *filter-only-master
+      - grafana-docker-master:
+          requires:
+            - build-all
+            - test-backend
+            - test-frontend
+            - codespell
+            - gometalinter
+            - mysql-integration-test
+            - postgres-integration-test
+          filters: *filter-only-master
       - deploy-enterprise-master:
           requires:
             - build-all
@@ -344,6 +366,16 @@ workflows:
             - mysql-integration-test
             - postgres-integration-test
           filters: *filter-only-release
+      - grafana-docker-release:
+          requires:
+            - build-all
+            - test-backend
+            - test-frontend
+            - codespell
+            - gometalinter
+            - mysql-integration-test
+            - postgres-integration-test
+          filters: *filter-only-release
 
   build-branches-and-prs:
       jobs:
@@ -361,15 +393,3 @@ workflows:
             filters: *filter-not-release-or-master
         - postgres-integration-test:
             filters: *filter-not-release-or-master
-        - grafana-docker-master:
-            requires:
-              - build
-              - test-backend
-              - test-frontend
-              - codespell
-              - gometalinter
-              - mysql-integration-test
-              - postgres-integration-test
-            filters:
-              branches:
-                only: grafana-docker
diff --git a/packaging/docker/README.md b/packaging/docker/README.md
new file mode 100644
index 00000000000..d80cd87aebc
--- /dev/null
+++ b/packaging/docker/README.md
@@ -0,0 +1,45 @@
+# Grafana Docker image
+
+[![CircleCI](https://circleci.com/gh/grafana/grafana-docker.svg?style=svg)](https://circleci.com/gh/grafana/grafana-docker)
+
+## Running your Grafana container
+
+Start your container binding the external port `3000`.
+
+```bash
+docker run -d --name=grafana -p 3000:3000 grafana/grafana
+```
+
+Try it out, default admin user is admin/admin.
+
+## How to use the container
+
+Further documentation can be found at http://docs.grafana.org/installation/docker/
+
+## Changelog
+
+### v5.1.5, v5.2.0-beta2
+* Fix: config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170)
+
+### v5.2.0-beta1
+* Support for Docker Secrets
+
+### v5.1.0
+* Major restructuring of the container
+* Usage of `chown` removed
+* File permissions incompatibility with previous versions
+  * user id changed from 104 to 472
+  * group id changed from 107 to 472
+* Runs as the grafana user by default (instead of root)
+* All default volumes removed
+
+### v4.2.0
+* Plugins are now installed into ${GF_PATHS_PLUGINS}
+* Building the container now requires a full url to the deb package instead of just version
+* Fixes bug caused by installing multiple plugins
+
+### v4.0.0-beta2
+* Plugins dir (`/var/lib/grafana/plugins`) is no longer a separate volume
+
+### v3.1.1
+* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026
\ No newline at end of file
diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh
index 923b1b8f3c0..e20ae2c2a41 100755
--- a/packaging/docker/build-deploy.sh
+++ b/packaging/docker/build-deploy.sh
@@ -4,8 +4,7 @@ _grafana_version=$1
 ./build.sh "$_grafana_version"
 docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
 
-#./push_to_docker_hub.sh "$_grafana_version"
-echo "Would have deployed $_grafana_version"
+./push_to_docker_hub.sh "$_grafana_version"
 
 if echo "$_grafana_version" | grep -q "^master-"; then
   apk add --no-cache curl
diff --git a/packaging/docker/custom/Dockerfile b/packaging/docker/custom/Dockerfile
new file mode 100644
index 00000000000..79eba5f29e9
--- /dev/null
+++ b/packaging/docker/custom/Dockerfile
@@ -0,0 +1,16 @@
+ARG GRAFANA_VERSION="latest"
+
+FROM grafana/grafana:${GRAFANA_VERSION}
+
+USER grafana
+
+ARG GF_INSTALL_PLUGINS=""
+
+RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \
+    OLDIFS=$IFS; \
+        IFS=','; \
+    for plugin in ${GF_INSTALL_PLUGINS}; do \
+        IFS=$OLDIFS; \
+        grafana-cli --pluginsDir "$GF_PATHS_PLUGINS" plugins install ${plugin}; \
+    done; \
+fi

From b61ac546f157a0b00d49ce11f5d86f8345034a38 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 30 Jul 2018 18:54:34 +0200
Subject: [PATCH 195/380] build: disables external docker build for master and
 release.

---
 .circleci/config.yml | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 818f30f7eea..e2deab62c1b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -260,9 +260,6 @@ jobs:
       - run:
           name: Trigger Windows build
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
-      - run:
-          name: Trigger Docker build
-          command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} master-$(echo "${CIRCLE_SHA1}" | cut -b1-7)'
       - run:
           name: Publish to Grafana.com
           command: |
@@ -284,9 +281,6 @@ jobs:
       - run:
           name: Trigger Windows build
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release'
-      - run:
-          name: Trigger Docker build
-          command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} ${CIRCLE_TAG}'
 
 workflows:
   version: 2

From bfc66a7ed0b395762eb72f4304569e4041711d8e Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Mon, 30 Jul 2018 11:04:04 +0200
Subject: [PATCH 196/380] add fillmode "last" to sql datasource

This adds a new fill mode last (last observation carried forward) for grafana
to the sql datasources. This fill mode will fill in the last seen value in a
series when a timepoint is missing or NULL if no value for that series has
been seen yet.
---
 docs/sources/features/datasources/mssql.md    |  4 ++-
 docs/sources/features/datasources/mysql.md    |  4 ++-
 docs/sources/features/datasources/postgres.md |  4 ++-
 pkg/tsdb/mssql/macros.go                      | 10 ++++--
 pkg/tsdb/mssql/macros_test.go                 | 17 ++++++++--
 pkg/tsdb/mysql/macros.go                      | 10 ++++--
 pkg/tsdb/mysql/mysql_test.go                  | 31 ++++++++++++++++++-
 pkg/tsdb/postgres/macros.go                   | 10 ++++--
 pkg/tsdb/postgres/postgres_test.go            | 30 +++++++++++++++++-
 pkg/tsdb/sql_engine.go                        | 24 +++++++++++++-
 .../mssql/partials/query.editor.html          |  4 ++-
 .../mysql/partials/query.editor.html          |  4 ++-
 .../postgres/partials/query.editor.html       |  4 ++-
 13 files changed, 136 insertions(+), 20 deletions(-)

diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
index dabb896ec0f..524a93a943b 100644
--- a/docs/sources/features/datasources/mssql.md
+++ b/docs/sources/features/datasources/mssql.md
@@ -81,7 +81,9 @@ Macro example | Description
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value. <br/>For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*.
-*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used.
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index a0e67037005..153b3d7bbf5 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -64,7 +64,9 @@ Macro example | Description
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
-*$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used.
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index 35dfcac15c0..b776b7cbe58 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -61,7 +61,9 @@ Macro example | Description
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300*
-*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
+*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
+*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
+*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used.
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index f33ab1d40be..57a37d618e0 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -99,9 +99,13 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 		if len(args) == 3 {
 			m.query.Model.Set("fill", true)
 			m.query.Model.Set("fillInterval", interval.Seconds())
-			if args[2] == "NULL" {
-				m.query.Model.Set("fillNull", true)
-			} else {
+			switch args[2] {
+			case "NULL":
+				m.query.Model.Set("fillMode", "null")
+			case "last":
+				m.query.Model.Set("fillMode", "last")
+			default:
+				m.query.Model.Set("fillMode", "value")
 				floatVal, err := strconv.ParseFloat(args[2], 64)
 				if err != nil {
 					return "", fmt.Errorf("error parsing fill value %v", args[2])
diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go
index ea50c418de7..b808666d967 100644
--- a/pkg/tsdb/mssql/macros_test.go
+++ b/pkg/tsdb/mssql/macros_test.go
@@ -76,12 +76,25 @@ func TestMacroEngine(t *testing.T) {
 				_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)")
 
 				fill := query.Model.Get("fill").MustBool()
-				fillNull := query.Model.Get("fillNull").MustBool()
+				fillMode := query.Model.Get("fillMode").MustString()
 				fillInterval := query.Model.Get("fillInterval").MustInt()
 
 				So(err, ShouldBeNil)
 				So(fill, ShouldBeTrue)
-				So(fillNull, ShouldBeTrue)
+				So(fillMode, ShouldEqual, "null")
+				So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
+			})
+
+			Convey("interpolate __timeGroup function with fill (value = last)", func() {
+				_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', last)")
+
+				fill := query.Model.Get("fill").MustBool()
+				fillMode := query.Model.Get("fillMode").MustString()
+				fillInterval := query.Model.Get("fillInterval").MustInt()
+
+				So(err, ShouldBeNil)
+				So(fill, ShouldBeTrue)
+				So(fillMode, ShouldEqual, "last")
 				So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
 			})
 
diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index a56fd1ceb2a..bebf4b396bb 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -94,9 +94,13 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 		if len(args) == 3 {
 			m.query.Model.Set("fill", true)
 			m.query.Model.Set("fillInterval", interval.Seconds())
-			if args[2] == "NULL" {
-				m.query.Model.Set("fillNull", true)
-			} else {
+			switch args[2] {
+			case "NULL":
+				m.query.Model.Set("fillMode", "null")
+			case "last":
+				m.query.Model.Set("fillMode", "last")
+			default:
+				m.query.Model.Set("fillMode", "value")
 				floatVal, err := strconv.ParseFloat(args[2], 64)
 				if err != nil {
 					return "", fmt.Errorf("error parsing fill value %v", args[2])
diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
index 9947c23498b..fe262a3f758 100644
--- a/pkg/tsdb/mysql/mysql_test.go
+++ b/pkg/tsdb/mysql/mysql_test.go
@@ -295,7 +295,7 @@ func TestMySQL(t *testing.T) {
 
 			})
 
-			Convey("When doing a metric query using timeGroup with float fill enabled", func() {
+			Convey("When doing a metric query using timeGroup with value fill enabled", func() {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
@@ -320,6 +320,35 @@ func TestMySQL(t *testing.T) {
 				points := queryResult.Series[0].Points
 				So(points[3][0].Float64, ShouldEqual, 1.5)
 			})
+
+			Convey("When doing a metric query using timeGroup with last fill enabled", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT $__timeGroup(time, '5m', last) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				points := queryResult.Series[0].Points
+				So(points[2][0].Float64, ShouldEqual, 15.0)
+				So(points[3][0].Float64, ShouldEqual, 15.0)
+				So(points[6][0].Float64, ShouldEqual, 20.0)
+			})
+
 		})
 
 		Convey("Given a table with metrics having multiple values and measurements", func() {
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 9e337caf3ec..3ab21ea0c6e 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -116,9 +116,13 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		if len(args) == 3 {
 			m.query.Model.Set("fill", true)
 			m.query.Model.Set("fillInterval", interval.Seconds())
-			if args[2] == "NULL" {
-				m.query.Model.Set("fillNull", true)
-			} else {
+			switch args[2] {
+			case "NULL":
+				m.query.Model.Set("fillMode", "null")
+			case "last":
+				m.query.Model.Set("fillMode", "last")
+			default:
+				m.query.Model.Set("fillMode", "value")
 				floatVal, err := strconv.ParseFloat(args[2], 64)
 				if err != nil {
 					return "", fmt.Errorf("error parsing fill value %v", args[2])
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index 3e864dca1e6..ac0964e912c 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -276,7 +276,7 @@ func TestPostgres(t *testing.T) {
 
 			})
 
-			Convey("When doing a metric query using timeGroup with float fill enabled", func() {
+			Convey("When doing a metric query using timeGroup with value fill enabled", func() {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
@@ -303,6 +303,34 @@ func TestPostgres(t *testing.T) {
 			})
 		})
 
+		Convey("When doing a metric query using timeGroup with last fill enabled", func() {
+			query := &tsdb.TsdbQuery{
+				Queries: []*tsdb.Query{
+					{
+						Model: simplejson.NewFromAny(map[string]interface{}{
+							"rawSql": "SELECT $__timeGroup(time, '5m', last), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+							"format": "time_series",
+						}),
+						RefId: "A",
+					},
+				},
+				TimeRange: &tsdb.TimeRange{
+					From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+					To:   fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
+				},
+			}
+
+			resp, err := endpoint.Query(nil, nil, query)
+			So(err, ShouldBeNil)
+			queryResult := resp.Results["A"]
+			So(queryResult.Error, ShouldBeNil)
+
+			points := queryResult.Series[0].Points
+			So(points[2][0].Float64, ShouldEqual, 15.0)
+			So(points[3][0].Float64, ShouldEqual, 15.0)
+			So(points[6][0].Float64, ShouldEqual, 20.0)
+		})
+
 		Convey("Given a table with metrics having multiple values and measurements", func() {
 			type metric_values struct {
 				Time                time.Time
diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index 3f681a5cdd7..f2f8b17db5f 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -274,9 +274,15 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 	fillMissing := query.Model.Get("fill").MustBool(false)
 	var fillInterval float64
 	fillValue := null.Float{}
+	fillLast := false
+
 	if fillMissing {
 		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
-		if !query.Model.Get("fillNull").MustBool(false) {
+		switch query.Model.Get("fillMode").MustString() {
+		case "null":
+		case "last":
+			fillLast = true
+		case "value":
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 		}
@@ -352,6 +358,14 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
 				}
 
+				if fillLast {
+					if len(series.Points) > 0 {
+						fillValue = series.Points[len(series.Points)-1][0]
+					} else {
+						fillValue.Valid = false
+					}
+				}
+
 				// align interval start
 				intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
 
@@ -377,6 +391,14 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 			intervalStart := series.Points[len(series.Points)-1][1].Float64
 			intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
 
+			if fillLast {
+				if len(series.Points) > 0 {
+					fillValue = series.Points[len(series.Points)-1][0]
+				} else {
+					fillValue.Valid = false
+				}
+			}
+
 			// align interval start
 			intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
 			for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval {
diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html
index e1320aabde2..e873d60ebbf 100644
--- a/public/app/plugins/datasource/mssql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mssql/partials/query.editor.html
@@ -53,7 +53,9 @@ Macros:
 - $__timeEpoch(column) -&gt; DATEDIFF(second, '1970-01-01', column) AS time
 - $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
 - $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
-- $__timeGroup(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.
+- $__timeGroup(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300.
+     by setting fillvalue grafana will fill in missing values according to the interval
+     fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time]
 
 Example of group by and order by with $__timeGroup:
diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html
index db12a3fe8ce..664481ec8dc 100644
--- a/public/app/plugins/datasource/mysql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mysql/partials/query.editor.html
@@ -53,7 +53,9 @@ Macros:
 - $__timeEpoch(column) -&gt; UNIX_TIMESTAMP(column) as time_sec
 - $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
 - $__unixEpochFilter(column) -&gt;  time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
-- $__timeGroup(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
+- $__timeGroup(column,'5m'[, fillvalue]) -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
+     by setting fillvalue grafana will fill in missing values according to the interval
+     fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time"
 
 Example of group by and order by with $__timeGroup:
diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html
index 1b7278f6809..c455c0ebaf9 100644
--- a/public/app/plugins/datasource/postgres/partials/query.editor.html
+++ b/public/app/plugins/datasource/postgres/partials/query.editor.html
@@ -53,7 +53,9 @@ Macros:
 - $__timeEpoch -&gt; extract(epoch from column) as "time"
 - $__timeFilter(column) -&gt; column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
 - $__unixEpochFilter(column) -&gt;  column &gt;= 1492750877 AND column &lt;= 1492750877
-- $__timeGroup(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300
+- $__timeGroup(column,'5m'[, fillvalue]) -&gt; (extract(epoch from column)/300)::bigint*300
+     by setting fillvalue grafana will fill in missing values according to the interval
+     fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300 AS "time"
 
 Example of group by and order by with $__timeGroup:

From 83d7ec1da2b9a00a542e955f6a41d4a6dbf75c63 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 31 Jul 2018 06:36:45 +0200
Subject: [PATCH 197/380] specify grafana version for last fill mode

---
 docs/sources/features/datasources/mssql.md    | 2 +-
 docs/sources/features/datasources/mysql.md    | 2 +-
 docs/sources/features/datasources/postgres.md | 1 +
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
index 524a93a943b..9a149df120d 100644
--- a/docs/sources/features/datasources/mssql.md
+++ b/docs/sources/features/datasources/mssql.md
@@ -83,7 +83,7 @@ Macro example | Description
 *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value. <br/>For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*.
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
 *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
-*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used.
+*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index 153b3d7bbf5..4f4efb6e29a 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -66,7 +66,7 @@ Macro example | Description
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
 *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
-*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used.
+*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index b776b7cbe58..f2b54d3f0ce 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -64,6 +64,7 @@ Macro example | Description
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
 *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
 *$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used.
+*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*

From 0ff54d257ade3d5a4fb3369dc2c6509378533a39 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Thu, 2 Aug 2018 17:42:28 +0200
Subject: [PATCH 198/380] build: makes it easier to build a local docker
 container.

---
 .gitignore                  | 1 +
 Makefile                    | 8 +++++++-
 packaging/docker/Dockerfile | 9 +++++----
 3 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/.gitignore b/.gitignore
index 11df66360d9..2484176a469 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,7 @@ debug.test
 /examples/*/dist
 /packaging/**/*.rpm
 /packaging/**/*.deb
+/packaging/**/*.tar.gz
 
 # Ignore OSX indexing
 .DS_Store
diff --git a/Makefile b/Makefile
index c1d755d247d..9e136688eb7 100644
--- a/Makefile
+++ b/Makefile
@@ -24,6 +24,12 @@ build-js:
 
 build: build-go build-js
 
+build-docker-dev:
+	@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
+	go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest
+	cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+	cd packaging/docker && docker build --tag grafana/grafana:dev .
+
 test-go:
 	go test -v ./pkg/...
 
@@ -36,4 +42,4 @@ run:
 	./bin/grafana-server
 
 protoc:
-	protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/.
\ No newline at end of file
+	protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/.
diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile
index 3025b03f920..aaaf333fc6b 100644
--- a/packaging/docker/Dockerfile
+++ b/packaging/docker/Dockerfile
@@ -12,14 +12,15 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
     GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
     GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
 
+RUN apt-get update && apt-get install -qq -y tar libfontconfig ca-certificates && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/*
+
 COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
 
-RUN apt-get update && apt-get install -qq -y tar libfontconfig ca-certificates && \
-    mkdir -p "$GF_PATHS_HOME/.aws" && \
+RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
     tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C "$GF_PATHS_HOME" && \
     rm /tmp/grafana.tar.gz && \
-    apt-get autoremove -y && \
-    rm -rf /var/lib/apt/lists/* && \
     groupadd -r -g $GF_GID grafana && \
     useradd -r -u $GF_UID -g grafana grafana && \
     mkdir -p "$GF_PATHS_PROVISIONING/datasources" \

From bda49fcaa209f9136659814d6900b3d156c2adca Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Fri, 3 Aug 2018 10:20:13 +0200
Subject: [PATCH 199/380] Add click on explore table cell to add filter to
 query (#12729)

* Add click on explore table cell to add filter to query

- move query state from query row to explore container to be able to set
  modified queries
- added TS interface for columns in table model
- plumbing from table cell click to datasource
- add modifyQuery to prometheus datasource
- implement addFilter as addLabelToQuery with tests

* Review feedback

- using airbnb style for Cell declaration
- fixed addLabelToQuery for complex label values
---
 public/app/containers/Explore/Explore.tsx     | 31 ++++++--
 public/app/containers/Explore/QueryRows.tsx   | 18 +----
 public/app/containers/Explore/Table.tsx       | 52 +++++++++++--
 public/app/core/table_model.ts                | 12 ++-
 .../datasource/prometheus/datasource.ts       | 74 +++++++++++++++++++
 .../prometheus/result_transformer.ts          |  2 +-
 .../prometheus/specs/datasource.jest.ts       | 27 ++++++-
 .../specs/result_transformer.jest.ts          |  6 +-
 public/sass/pages/_explore.scss               |  4 +
 9 files changed, 190 insertions(+), 36 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 178e53198d4..a0bb38a13f1 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -187,11 +187,14 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setDatasource(datasource);
   };
 
-  handleChangeQuery = (query, index) => {
+  handleChangeQuery = (value, index) => {
     const { queries } = this.state;
+    const prevQuery = queries[index];
+    const edited = prevQuery.query !== value;
     const nextQuery = {
       ...queries[index],
-      query,
+      edited,
+      query: value,
     };
     const nextQueries = [...queries];
     nextQueries[index] = nextQuery;
@@ -254,6 +257,18 @@ export class Explore extends React.Component<any, IExploreState> {
     }
   };
 
+  onClickTableCell = (columnKey: string, rowValue: string) => {
+    const { datasource, queries } = this.state;
+    if (datasource && datasource.modifyQuery) {
+      const nextQueries = queries.map(q => ({
+        ...q,
+        edited: false,
+        query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }),
+      }));
+      this.setState({ queries: nextQueries }, () => this.handleSubmit());
+    }
+  };
+
   buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
     const { datasource, queries, range } = this.state;
     const resolution = this.el.offsetWidth;
@@ -390,12 +405,12 @@ export class Explore extends React.Component<any, IExploreState> {
               </a>
             </div>
           ) : (
-            <div className="navbar-buttons explore-first-button">
-              <button className="btn navbar-button" onClick={this.handleClickCloseSplit}>
-                Close Split
+              <div className="navbar-buttons explore-first-button">
+                <button className="btn navbar-button" onClick={this.handleClickCloseSplit}>
+                  Close Split
               </button>
-            </div>
-          )}
+              </div>
+            )}
           {!datasourceMissing ? (
             <div className="navbar-buttons">
               <Select
@@ -473,7 +488,7 @@ export class Explore extends React.Component<any, IExploreState> {
                   split={split}
                 />
               ) : null}
-              {supportsTable && showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
+              {supportsTable && showingTable ? <Table data={tableResult} onClickCell={this.onClickTableCell} className="m-t-3" /> : null}
               {supportsLogs && showingLogs ? <Logs data={logsResult} /> : null}
             </main>
           </div>
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx
index 3aaa006d6df..d2c1d81607f 100644
--- a/public/app/containers/Explore/QueryRows.tsx
+++ b/public/app/containers/Explore/QueryRows.tsx
@@ -3,19 +3,8 @@ import React, { PureComponent } from 'react';
 import QueryField from './PromQueryField';
 
 class QueryRow extends PureComponent<any, any> {
-  constructor(props) {
-    super(props);
-    this.state = {
-      edited: false,
-      query: props.query || '',
-    };
-  }
-
   handleChangeQuery = value => {
     const { index, onChangeQuery } = this.props;
-    const { query } = this.state;
-    const edited = query !== value;
-    this.setState({ edited, query: value });
     if (onChangeQuery) {
       onChangeQuery(value, index);
     }
@@ -43,8 +32,7 @@ class QueryRow extends PureComponent<any, any> {
   };
 
   render() {
-    const { request } = this.props;
-    const { edited, query } = this.state;
+    const { request, query, edited } = this.props;
     return (
       <div className="query-row">
         <div className="query-row-tools">
@@ -74,7 +62,9 @@ export default class QueryRows extends PureComponent<any, any> {
     const { className = '', queries, ...handlers } = this.props;
     return (
       <div className={className}>
-        {queries.map((q, index) => <QueryRow key={q.key} index={index} query={q.query} {...handlers} />)}
+        {queries.map((q, index) => (
+          <QueryRow key={q.key} index={index} query={q.query} edited={q.edited} {...handlers} />
+        ))}
       </div>
     );
   }
diff --git a/public/app/containers/Explore/Table.tsx b/public/app/containers/Explore/Table.tsx
index 7179a0fc89a..0856acd5d89 100644
--- a/public/app/containers/Explore/Table.tsx
+++ b/public/app/containers/Explore/Table.tsx
@@ -1,14 +1,44 @@
 import React, { PureComponent } from 'react';
-// import TableModel from 'app/core/table_model';
+import TableModel from 'app/core/table_model';
 
-const EMPTY_TABLE = {
-  columns: [],
-  rows: [],
-};
+const EMPTY_TABLE = new TableModel();
 
-export default class Table extends PureComponent<any, any> {
+interface TableProps {
+  className?: string;
+  data: TableModel;
+  onClickCell?: (columnKey: string, rowValue: string) => void;
+}
+
+interface SFCCellProps {
+  columnIndex: number;
+  onClickCell?: (columnKey: string, rowValue: string, columnIndex: number, rowIndex: number, table: TableModel) => void;
+  rowIndex: number;
+  table: TableModel;
+  value: string;
+}
+
+function Cell(props: SFCCellProps) {
+  const { columnIndex, rowIndex, table, value, onClickCell } = props;
+  const column = table.columns[columnIndex];
+  if (column && column.filterable && onClickCell) {
+    const onClick = event => {
+      event.preventDefault();
+      onClickCell(column.text, value, columnIndex, rowIndex, table);
+    };
+    return (
+      <td>
+        <a className="link" onClick={onClick}>
+          {value}
+        </a>
+      </td>
+    );
+  }
+  return <td>{value}</td>;
+}
+
+export default class Table extends PureComponent<TableProps, {}> {
   render() {
-    const { className = '', data } = this.props;
+    const { className = '', data, onClickCell } = this.props;
     const tableModel = data || EMPTY_TABLE;
     return (
       <table className={`${className} filter-table`}>
@@ -16,7 +46,13 @@ export default class Table extends PureComponent<any, any> {
           <tr>{tableModel.columns.map(col => <th key={col.text}>{col.text}</th>)}</tr>
         </thead>
         <tbody>
-          {tableModel.rows.map((row, i) => <tr key={i}>{row.map((content, j) => <td key={j}>{content}</td>)}</tr>)}
+          {tableModel.rows.map((row, i) => (
+            <tr key={i}>
+              {row.map((value, j) => (
+                <Cell key={j} columnIndex={j} rowIndex={i} value={value} table={data} onClickCell={onClickCell} />
+              ))}
+            </tr>
+          ))}
         </tbody>
       </table>
     );
diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts
index 04857eb806d..0c85a0293dd 100644
--- a/public/app/core/table_model.ts
+++ b/public/app/core/table_model.ts
@@ -1,5 +1,15 @@
+interface Column {
+  text: string;
+  title?: string;
+  type?: string;
+  sort?: boolean;
+  desc?: boolean;
+  filterable?: boolean;
+  unit?: string;
+}
+
 export default class TableModel {
-  columns: any[];
+  columns: Column[];
   rows: any[];
   type: string;
   columnMap: any;
diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index ac8d774db59..fc8f3999856 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -16,6 +16,72 @@ export function alignRange(start, end, step) {
   };
 }
 
+const keywords = 'by|without|on|ignoring|group_left|group_right';
+
+// Duplicate from mode-prometheus.js, which can't be used in tests due to global ace not being loaded.
+const builtInWords = [
+  keywords,
+  'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile',
+  'true|false|null|__name__|job',
+  'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv',
+  'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2',
+  'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time',
+  'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time',
+]
+  .join('|')
+  .split('|');
+
+// addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}'
+export function addLabelToQuery(query: string, key: string, value: string): string {
+  if (!key || !value) {
+    throw new Error('Need label to add to query.');
+  }
+
+  // Add empty selector to bare metric name
+  let previousWord;
+  query = query.replace(/(\w+)\b(?![\({=",])/g, (match, word, offset) => {
+    // Check if inside a selector
+    const nextSelectorStart = query.slice(offset).indexOf('{');
+    const nextSelectorEnd = query.slice(offset).indexOf('}');
+    const insideSelector = nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd);
+    // Handle "sum by (key) (metric)"
+    const previousWordIsKeyWord = previousWord && keywords.split('|').indexOf(previousWord) > -1;
+    previousWord = word;
+    if (!insideSelector && !previousWordIsKeyWord && builtInWords.indexOf(word) === -1) {
+      return `${word}{}`;
+    }
+    return word;
+  });
+
+  // Adding label to existing selectors
+  const selectorRegexp = /{([^{]*)}/g;
+  let match = null;
+  const parts = [];
+  let lastIndex = 0;
+  let suffix = '';
+  while ((match = selectorRegexp.exec(query))) {
+    const prefix = query.slice(lastIndex, match.index);
+    const selectorParts = match[1].split(',');
+    const labels = selectorParts.reduce((acc, label) => {
+      const labelParts = label.split('=');
+      if (labelParts.length === 2) {
+        acc[labelParts[0]] = labelParts[1];
+      }
+      return acc;
+    }, {});
+    labels[key] = `"${value}"`;
+    const selector = Object.keys(labels)
+      .sort()
+      .map(key => `${key}=${labels[key]}`)
+      .join(',');
+    lastIndex = match.index + match[1].length + 2;
+    suffix = query.slice(match.index + match[0].length);
+    parts.push(prefix, '{', selector, '}');
+  }
+  parts.push(suffix);
+  return parts.join('');
+}
+
 export function prometheusRegularEscape(value) {
   if (typeof value === 'string') {
     return value.replace(/'/g, "\\\\'");
@@ -384,6 +450,14 @@ export class PrometheusDatasource {
     return state;
   }
 
+  modifyQuery(query: string, options: any): string {
+    const { addFilter } = options;
+    if (addFilter) {
+      return addLabelToQuery(query, addFilter.key, addFilter.value);
+    }
+    return query;
+  }
+
   getPrometheusTime(date, roundUp) {
     if (_.isString(date)) {
       date = dateMath.parse(date, roundUp);
diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts
index b6d8a32af5f..a7c6703c10f 100644
--- a/public/app/plugins/datasource/prometheus/result_transformer.ts
+++ b/public/app/plugins/datasource/prometheus/result_transformer.ts
@@ -86,7 +86,7 @@ export class ResultTransformer {
     table.columns.push({ text: 'Time', type: 'time' });
     _.each(sortedLabels, function(label, labelIndex) {
       metricLabels[label] = labelIndex + 1;
-      table.columns.push({ text: label });
+      table.columns.push({ text: label, filterable: !label.startsWith('__') });
     });
     let valueText = resultCount > 1 ? `Value #${refId}` : 'Value';
     table.columns.push({ text: valueText });
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index aeca8d69191..b946a6f5e7e 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -1,7 +1,14 @@
 import _ from 'lodash';
 import moment from 'moment';
 import q from 'q';
-import { alignRange, PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
+import {
+  alignRange,
+  PrometheusDatasource,
+  prometheusSpecialRegexEscape,
+  prometheusRegularEscape,
+  addLabelToQuery,
+} from '../datasource';
+
 jest.mock('../metric_find_query');
 
 describe('PrometheusDatasource', () => {
@@ -245,6 +252,24 @@ describe('PrometheusDatasource', () => {
       expect(intervalMs).toEqual({ text: 15000, value: 15000 });
     });
   });
+
+  describe('addLabelToQuery()', () => {
+    expect(() => {
+      addLabelToQuery('foo', '', '');
+    }).toThrow();
+    expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}');
+    expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}');
+    expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"}');
+    expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"} + metric{bar="baz"}');
+    expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})');
+    expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe(
+      'foo{bar="baz",x="yy"} * metric{a="bb",bar="baz",y="zz"} * metric2{bar="baz"}'
+    );
+    expect(addLabelToQuery('sum by (xx) (foo)', 'bar', 'baz')).toBe('sum by (xx) (foo{bar="baz"})');
+    expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe(
+      'foo{bar="baz",instance="my-host.com:9100"}'
+    );
+  });
 });
 
 const SECOND = 1000;
diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts
index c0f2609f5b4..e2a21a8f866 100644
--- a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts
@@ -39,7 +39,7 @@ describe('Prometheus Result Transformer', () => {
         [1443454528000, 'test', '', 'testjob', 3846],
         [1443454529000, 'test', 'localhost:8080', 'otherjob', 3847],
       ]);
-      expect(table.columns).toEqual([
+      expect(table.columns).toMatchObject([
         { text: 'Time', type: 'time' },
         { text: '__name__' },
         { text: 'instance' },
@@ -51,7 +51,7 @@ describe('Prometheus Result Transformer', () => {
     it('should column title include refId if response count is more than 2', () => {
       var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result, 2, 'B');
       expect(table.type).toBe('table');
-      expect(table.columns).toEqual([
+      expect(table.columns).toMatchObject([
         { text: 'Time', type: 'time' },
         { text: '__name__' },
         { text: 'instance' },
@@ -79,7 +79,7 @@ describe('Prometheus Result Transformer', () => {
       var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result);
       expect(table.type).toBe('table');
       expect(table.rows).toEqual([[1443454528000, 'test', 'testjob', 3846]]);
-      expect(table.columns).toEqual([
+      expect(table.columns).toMatchObject([
         { text: 'Time', type: 'time' },
         { text: '__name__' },
         { text: 'job' },
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index 158f0eb68ad..59b8b62f349 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -80,6 +80,10 @@
   .relative {
     position: relative;
   }
+
+  .link {
+    text-decoration: underline;
+  }
 }
 
 .explore + .explore {

From 61e3a0ccebef255c48da2b951f1cfb23628c96a4 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 11:57:03 +0200
Subject: [PATCH 200/380] Begin conversion

---
 public/test/specs/app.jest.ts | 10 ++++++++++
 1 file changed, 10 insertions(+)
 create mode 100644 public/test/specs/app.jest.ts

diff --git a/public/test/specs/app.jest.ts b/public/test/specs/app.jest.ts
new file mode 100644
index 00000000000..0e15ab8234d
--- /dev/null
+++ b/public/test/specs/app.jest.ts
@@ -0,0 +1,10 @@
+import { GrafanaApp } from 'app/app';
+jest.mock('app/routes/routes');
+
+describe('GrafanaApp', () => {
+  var app = new GrafanaApp();
+
+  it('can call inits', () => {
+    expect(app).not.toBe(null);
+  });
+});

From 5bea54eaaa404f7eef95d798cd87a5c52fae3294 Mon Sep 17 00:00:00 2001
From: Emil Flink <emil.flink@bublar.com>
Date: Fri, 3 Aug 2018 12:00:20 +0200
Subject: [PATCH 201/380] Support client certificates for LDAP servers

---
 conf/ldap.toml                    |  3 +++
 docs/sources/installation/ldap.md |  3 +++
 pkg/login/ldap.go                 | 10 ++++++++++
 pkg/login/ldap_settings.go        |  2 ++
 4 files changed, 18 insertions(+)

diff --git a/conf/ldap.toml b/conf/ldap.toml
index a74b2b6cc2c..9a7088ed823 100644
--- a/conf/ldap.toml
+++ b/conf/ldap.toml
@@ -15,6 +15,9 @@ start_tls = false
 ssl_skip_verify = false
 # set to the path to your root CA certificate or leave unset to use system defaults
 # root_ca_cert = "/path/to/certificate.crt"
+# Authentication against LDAP servers requiring client certificates
+# client_cert = "/path/to/client.crt"
+# client_key = "/path/to/client.key"
 
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md
index 9a381b9e467..b555eaf06e0 100644
--- a/docs/sources/installation/ldap.md
+++ b/docs/sources/installation/ldap.md
@@ -40,6 +40,9 @@ start_tls = false
 ssl_skip_verify = false
 # set to the path to your root CA certificate or leave unset to use system defaults
 # root_ca_cert = "/path/to/certificate.crt"
+# Authentication against LDAP servers requiring client certificates
+# client_cert = "/path/to/client.crt"
+# client_key = "/path/to/client.key"
 
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go
index 9e4918f0290..053778e8deb 100644
--- a/pkg/login/ldap.go
+++ b/pkg/login/ldap.go
@@ -59,6 +59,13 @@ func (a *ldapAuther) Dial() error {
 			}
 		}
 	}
+	var clientCert tls.Certificate
+	if a.server.ClientCert != "" && a.server.ClientKey != "" {
+		clientCert, err = tls.LoadX509KeyPair(a.server.ClientCert, a.server.ClientKey)
+		if err != nil {
+			return err
+		}
+	}
 	for _, host := range strings.Split(a.server.Host, " ") {
 		address := fmt.Sprintf("%s:%d", host, a.server.Port)
 		if a.server.UseSSL {
@@ -67,6 +74,9 @@ func (a *ldapAuther) Dial() error {
 				ServerName:         host,
 				RootCAs:            certPool,
 			}
+			if len(clientCert.Certificate) > 0 {
+				tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
+			}
 			if a.server.StartTLS {
 				a.conn, err = ldap.Dial("tcp", address)
 				if err == nil {
diff --git a/pkg/login/ldap_settings.go b/pkg/login/ldap_settings.go
index c4f5982b237..7ebfbc79ba8 100644
--- a/pkg/login/ldap_settings.go
+++ b/pkg/login/ldap_settings.go
@@ -21,6 +21,8 @@ type LdapServerConf struct {
 	StartTLS      bool             `toml:"start_tls"`
 	SkipVerifySSL bool             `toml:"ssl_skip_verify"`
 	RootCACert    string           `toml:"root_ca_cert"`
+	ClientCert    string           `toml:"client_cert"`
+	ClientKey     string           `toml:"client_key"`
 	BindDN        string           `toml:"bind_dn"`
 	BindPassword  string           `toml:"bind_password"`
 	Attr          LdapAttributeMap `toml:"attributes"`

From 61eb96ed79818cb317beeba3f866262807412db3 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 12:34:13 +0200
Subject: [PATCH 202/380] Remove simple tests

---
 .../features/alerting/specs/alert_tab_specs.ts  | 17 -----------------
 .../dashboard/specs/dashboard_srv_specs.ts      | 15 ---------------
 public/test/specs/app.jest.ts                   | 10 ----------
 public/test/specs/app_specs.ts                  | 14 --------------
 4 files changed, 56 deletions(-)
 delete mode 100644 public/app/features/alerting/specs/alert_tab_specs.ts
 delete mode 100644 public/app/features/dashboard/specs/dashboard_srv_specs.ts
 delete mode 100644 public/test/specs/app.jest.ts
 delete mode 100644 public/test/specs/app_specs.ts

diff --git a/public/app/features/alerting/specs/alert_tab_specs.ts b/public/app/features/alerting/specs/alert_tab_specs.ts
deleted file mode 100644
index 4a4de34fe6c..00000000000
--- a/public/app/features/alerting/specs/alert_tab_specs.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { describe, it, expect } from 'test/lib/common';
-
-import { AlertTabCtrl } from '../alert_tab_ctrl';
-
-describe('AlertTabCtrl', () => {
-  var $scope = {
-    ctrl: {},
-  };
-
-  describe('with null parameters', () => {
-    it('can be created', () => {
-      var alertTab = new AlertTabCtrl($scope, null, null, null, null, null);
-
-      expect(alertTab).to.not.be(null);
-    });
-  });
-});
diff --git a/public/app/features/dashboard/specs/dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dashboard_srv_specs.ts
deleted file mode 100644
index 0faa7531652..00000000000
--- a/public/app/features/dashboard/specs/dashboard_srv_specs.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { describe, beforeEach, expect } from 'test/lib/common';
-
-import { DashboardSrv } from '../dashboard_srv';
-
-describe('dashboardSrv', function() {
-  var _dashboardSrv;
-
-  beforeEach(() => {
-    _dashboardSrv = new DashboardSrv({}, {}, {});
-  });
-
-  it('should do something', () => {
-    expect(_dashboardSrv).not.to.be(null);
-  });
-});
diff --git a/public/test/specs/app.jest.ts b/public/test/specs/app.jest.ts
deleted file mode 100644
index 0e15ab8234d..00000000000
--- a/public/test/specs/app.jest.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { GrafanaApp } from 'app/app';
-jest.mock('app/routes/routes');
-
-describe('GrafanaApp', () => {
-  var app = new GrafanaApp();
-
-  it('can call inits', () => {
-    expect(app).not.toBe(null);
-  });
-});
diff --git a/public/test/specs/app_specs.ts b/public/test/specs/app_specs.ts
deleted file mode 100644
index f82946c20a2..00000000000
--- a/public/test/specs/app_specs.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {describe, it, expect} from 'test/lib/common';
-
-import {GrafanaApp} from 'app/app';
-
-describe('GrafanaApp', () => {
-
-  var app = new GrafanaApp();
-
-  it('can call inits', () => {
-    expect(app).to.not.be(null);
-  });
-});
-
-

From c900a30106b8bc8ac109500d23c209df146a451f Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 3 Aug 2018 13:09:05 +0200
Subject: [PATCH 203/380] renamed slate unit tests to .jest.ts

---
 .../Explore/slate-plugins/{braces.test.ts => braces.jest.ts}      | 0
 .../Explore/slate-plugins/{clear.test.ts => clear.jest.ts}        | 0
 2 files changed, 0 insertions(+), 0 deletions(-)
 rename public/app/containers/Explore/slate-plugins/{braces.test.ts => braces.jest.ts} (100%)
 rename public/app/containers/Explore/slate-plugins/{clear.test.ts => clear.jest.ts} (100%)

diff --git a/public/app/containers/Explore/slate-plugins/braces.test.ts b/public/app/containers/Explore/slate-plugins/braces.jest.ts
similarity index 100%
rename from public/app/containers/Explore/slate-plugins/braces.test.ts
rename to public/app/containers/Explore/slate-plugins/braces.jest.ts
diff --git a/public/app/containers/Explore/slate-plugins/clear.test.ts b/public/app/containers/Explore/slate-plugins/clear.jest.ts
similarity index 100%
rename from public/app/containers/Explore/slate-plugins/clear.test.ts
rename to public/app/containers/Explore/slate-plugins/clear.jest.ts

From 818fe09a7f94e7b6c4ad7b36b1ce8f3348ef7598 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 2 Aug 2018 13:39:05 +0200
Subject: [PATCH 204/380] Fit panels to screen height

---
 .../app/features/dashboard/dashboard_ctrl.ts  | 30 ++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index 94d0b18f157..77e5ca88552 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -4,7 +4,8 @@ import coreModule from 'app/core/core_module';
 import { PanelContainer } from './dashgrid/PanelContainer';
 import { DashboardModel } from './dashboard_model';
 import { PanelModel } from './panel_model';
-
+import { GRID_CELL_HEIGHT } from 'app/core/constants';
+import { PanelLinksEditorCtrl } from '../panellinks/module';
 export class DashboardCtrl implements PanelContainer {
   dashboard: DashboardModel;
   dashboardViewState: any;
@@ -62,6 +63,33 @@ export class DashboardCtrl implements PanelContainer {
       .finally(() => {
         this.dashboard = dashboard;
         this.dashboard.processRepeats();
+        console.log(this.dashboard.panels);
+
+        let maxRows = Math.max(
+          ...this.dashboard.panels.map(panel => {
+            return panel.gridPos.h + panel.gridPos.y;
+          })
+        );
+        console.log('maxRows: ' + maxRows);
+        //Consider navbar and submenu controls
+        let availableHeight = window.innerHeight - 280;
+        let availableRows = Math.floor(availableHeight / GRID_CELL_HEIGHT);
+
+        console.log('availableRows: ' + availableRows);
+        if (maxRows > availableRows) {
+          let scaleFactor = maxRows / availableRows;
+          console.log(scaleFactor);
+
+          this.dashboard.panels.forEach((panel, i) => {
+            console.log(i);
+            console.log(panel.gridPos);
+            panel.gridPos.y = Math.floor(panel.gridPos.y / scaleFactor) || 1;
+            panel.gridPos.h = Math.floor(panel.gridPos.h / scaleFactor) || 1;
+
+            console.log(panel.gridPos);
+          });
+        }
+        console.log(this.dashboard.panels);
 
         this.unsavedChangesSrv.init(dashboard, this.$scope);
 

From a9f24bb36d487e7880864e65aaf26c277db11313 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 2 Aug 2018 13:52:10 +0200
Subject: [PATCH 205/380] Remove weird import

---
 public/app/features/dashboard/dashboard_ctrl.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index 77e5ca88552..04f1207d49a 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -5,7 +5,7 @@ import { PanelContainer } from './dashgrid/PanelContainer';
 import { DashboardModel } from './dashboard_model';
 import { PanelModel } from './panel_model';
 import { GRID_CELL_HEIGHT } from 'app/core/constants';
-import { PanelLinksEditorCtrl } from '../panellinks/module';
+
 export class DashboardCtrl implements PanelContainer {
   dashboard: DashboardModel;
   dashboardViewState: any;

From 78b3dc40f11fc8e3ff5d5094943c57502fc49aa7 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 2 Aug 2018 14:35:49 +0200
Subject: [PATCH 206/380] Add margin and padding compensation

---
 .../app/features/dashboard/dashboard_ctrl.ts  | 29 +++++++++----------
 1 file changed, 14 insertions(+), 15 deletions(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index 04f1207d49a..a3f9a7b33d5 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module';
 import { PanelContainer } from './dashgrid/PanelContainer';
 import { DashboardModel } from './dashboard_model';
 import { PanelModel } from './panel_model';
-import { GRID_CELL_HEIGHT } from 'app/core/constants';
+import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
 
 export class DashboardCtrl implements PanelContainer {
   dashboard: DashboardModel;
@@ -71,24 +71,23 @@ export class DashboardCtrl implements PanelContainer {
           })
         );
         console.log('maxRows: ' + maxRows);
-        //Consider navbar and submenu controls
-        let availableHeight = window.innerHeight - 280;
-        let availableRows = Math.floor(availableHeight / GRID_CELL_HEIGHT);
+        //Consider navbar and submenu controls, padding and margin
+        let availableHeight = window.innerHeight - 80 - 2 * GRID_CELL_VMARGIN;
+        let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
 
         console.log('availableRows: ' + availableRows);
-        if (maxRows > availableRows) {
-          let scaleFactor = maxRows / availableRows;
-          console.log(scaleFactor);
+        let scaleFactor = maxRows / availableRows;
+        console.log(scaleFactor);
 
-          this.dashboard.panels.forEach((panel, i) => {
-            console.log(i);
-            console.log(panel.gridPos);
-            panel.gridPos.y = Math.floor(panel.gridPos.y / scaleFactor) || 1;
-            panel.gridPos.h = Math.floor(panel.gridPos.h / scaleFactor) || 1;
+        this.dashboard.panels.forEach((panel, i) => {
+          console.log(i);
+          console.log(panel.gridPos);
+          panel.gridPos.y = Math.floor(panel.gridPos.y / scaleFactor) || 1;
+          panel.gridPos.h = Math.floor(panel.gridPos.h / scaleFactor) || 1;
+
+          console.log(panel.gridPos);
+        });
 
-            console.log(panel.gridPos);
-          });
-        }
         console.log(this.dashboard.panels);
 
         this.unsavedChangesSrv.init(dashboard, this.$scope);

From 9e4748e2aa5eff942c49d74d807063fee6b9e612 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 2 Aug 2018 14:46:02 +0200
Subject: [PATCH 207/380] Go with just single margin compensation

---
 public/app/features/dashboard/dashboard_ctrl.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index a3f9a7b33d5..51a42ab0b0a 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -72,7 +72,7 @@ export class DashboardCtrl implements PanelContainer {
         );
         console.log('maxRows: ' + maxRows);
         //Consider navbar and submenu controls, padding and margin
-        let availableHeight = window.innerHeight - 80 - 2 * GRID_CELL_VMARGIN;
+        let availableHeight = window.innerHeight - 80;
         let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
 
         console.log('availableRows: ' + availableRows);

From 338a37abc8d2dd59d2c470cd7bffe67c31f4caf0 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 2 Aug 2018 15:06:41 +0200
Subject: [PATCH 208/380] Replace floor with round

---
 public/app/features/dashboard/dashboard_ctrl.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index 51a42ab0b0a..bdd6d7050c5 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -82,8 +82,8 @@ export class DashboardCtrl implements PanelContainer {
         this.dashboard.panels.forEach((panel, i) => {
           console.log(i);
           console.log(panel.gridPos);
-          panel.gridPos.y = Math.floor(panel.gridPos.y / scaleFactor) || 1;
-          panel.gridPos.h = Math.floor(panel.gridPos.h / scaleFactor) || 1;
+          panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
+          panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
 
           console.log(panel.gridPos);
         });

From 63fa9fdc6d3fcfb8fd31450bb9f5880cd497fdf9 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Thu, 2 Aug 2018 15:55:03 +0200
Subject: [PATCH 209/380] Add temporary url parameter

---
 .../app/features/dashboard/dashboard_ctrl.ts  | 38 ++++++++-----------
 1 file changed, 15 insertions(+), 23 deletions(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index bdd6d7050c5..ef68a76da30 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -63,32 +63,24 @@ export class DashboardCtrl implements PanelContainer {
       .finally(() => {
         this.dashboard = dashboard;
         this.dashboard.processRepeats();
-        console.log(this.dashboard.panels);
 
-        let maxRows = Math.max(
-          ...this.dashboard.panels.map(panel => {
-            return panel.gridPos.h + panel.gridPos.y;
-          })
-        );
-        console.log('maxRows: ' + maxRows);
-        //Consider navbar and submenu controls, padding and margin
-        let availableHeight = window.innerHeight - 80;
-        let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
+        if (window.location.search.search('autofitpanels') !== -1) {
+          let maxRows = Math.max(
+            ...this.dashboard.panels.map(panel => {
+              return panel.gridPos.h + panel.gridPos.y;
+            })
+          );
 
-        console.log('availableRows: ' + availableRows);
-        let scaleFactor = maxRows / availableRows;
-        console.log(scaleFactor);
+          //Consider navbar and submenu controls, padding and margin
+          let availableHeight = window.innerHeight - 80;
+          let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
+          let scaleFactor = maxRows / availableRows;
 
-        this.dashboard.panels.forEach((panel, i) => {
-          console.log(i);
-          console.log(panel.gridPos);
-          panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
-          panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
-
-          console.log(panel.gridPos);
-        });
-
-        console.log(this.dashboard.panels);
+          this.dashboard.panels.forEach((panel, i) => {
+            panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
+            panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
+          });
+        }
 
         this.unsavedChangesSrv.init(dashboard, this.$scope);
 

From 1618b095c7701015a82ae4e4aeb597bec7007d24 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 11:00:27 +0200
Subject: [PATCH 210/380] Use  and add keybard shortcut

---
 public/app/core/services/keybindingSrv.ts       | 16 +++++++++++++++-
 public/app/features/dashboard/dashboard_ctrl.ts |  5 +++--
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index 672ae29740b..0930a16d797 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -15,7 +15,14 @@ export class KeybindingSrv {
   timepickerOpen = false;
 
   /** @ngInject */
-  constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) {
+  constructor(
+    private $rootScope,
+    private $location,
+    private datasourceSrv,
+    private timeSrv,
+    private contextSrv,
+    private $window
+  ) {
     // clear out all shortcuts on route change
     $rootScope.$on('$routeChangeSuccess', () => {
       Mousetrap.reset();
@@ -259,6 +266,13 @@ export class KeybindingSrv {
     this.bind('d v', () => {
       appEvents.emit('toggle-view-mode');
     });
+
+    //Autofit panels
+    this.bind('d a', () => {
+      this.$location.search('autofitpanels', this.$location.search().autofitpanels ? null : true);
+      //Force reload
+      this.$window.location.href = this.$location.absUrl();
+    });
   }
 }
 
diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index ef68a76da30..ccb5686b23a 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -24,7 +24,8 @@ export class DashboardCtrl implements PanelContainer {
     private unsavedChangesSrv,
     private dashboardViewStateSrv,
     public playlistSrv,
-    private panelLoader
+    private panelLoader,
+    private $location
   ) {
     // temp hack due to way dashboards are loaded
     // can't use controllerAs on route yet
@@ -64,7 +65,7 @@ export class DashboardCtrl implements PanelContainer {
         this.dashboard = dashboard;
         this.dashboard.processRepeats();
 
-        if (window.location.search.search('autofitpanels') !== -1) {
+        if (this.$location.search().autofitpanels) {
           let maxRows = Math.max(
             ...this.dashboard.panels.map(panel => {
               return panel.gridPos.h + panel.gridPos.y;

From 36c406eefb89d1963bcf54015b326b0cae3f5f0a Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 11:37:33 +0200
Subject: [PATCH 211/380] Extract to own method

---
 .../app/features/dashboard/dashboard_ctrl.ts  | 39 ++++++++++---------
 1 file changed, 21 insertions(+), 18 deletions(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index ccb5686b23a..16b21306a4e 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -65,24 +65,7 @@ export class DashboardCtrl implements PanelContainer {
         this.dashboard = dashboard;
         this.dashboard.processRepeats();
 
-        if (this.$location.search().autofitpanels) {
-          let maxRows = Math.max(
-            ...this.dashboard.panels.map(panel => {
-              return panel.gridPos.h + panel.gridPos.y;
-            })
-          );
-
-          //Consider navbar and submenu controls, padding and margin
-          let availableHeight = window.innerHeight - 80;
-          let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
-          let scaleFactor = maxRows / availableRows;
-
-          this.dashboard.panels.forEach((panel, i) => {
-            panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
-            panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
-          });
-        }
-
+        this.autofitPanels();
         this.unsavedChangesSrv.init(dashboard, this.$scope);
 
         // TODO refactor ViewStateSrv
@@ -99,6 +82,26 @@ export class DashboardCtrl implements PanelContainer {
       .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true));
   }
 
+  autofitPanels() {
+    if (this.$location.search().autofitpanels) {
+      let maxRows = Math.max(
+        ...this.dashboard.panels.map(panel => {
+          return panel.gridPos.h + panel.gridPos.y;
+        })
+      );
+
+      //Consider navbar and submenu controls, padding and margin
+      let availableHeight = window.innerHeight - 80;
+      let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
+      let scaleFactor = maxRows / availableRows;
+
+      this.dashboard.panels.forEach((panel, i) => {
+        panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
+        panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
+      });
+    }
+  }
+
   onInitFailed(msg, fatal, err) {
     console.log(msg, err);
 

From 4b84a585751fe955a039c2b910aff1b5b6385340 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 12:19:41 +0200
Subject: [PATCH 212/380] Disable submenu when autopanels is enabled

---
 public/app/features/dashboard/dashboard_ctrl.ts     |  4 +++-
 public/app/features/dashboard/dashboard_model.ts    |  5 +++++
 .../dashboard/specs/dashboard_model.jest.ts         | 13 +++++++++++++
 3 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index 16b21306a4e..9266794d6e4 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -91,7 +91,7 @@ export class DashboardCtrl implements PanelContainer {
       );
 
       //Consider navbar and submenu controls, padding and margin
-      let availableHeight = window.innerHeight - 80;
+      let availableHeight = window.innerHeight - 40;
       let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
       let scaleFactor = maxRows / availableRows;
 
@@ -99,6 +99,8 @@ export class DashboardCtrl implements PanelContainer {
         panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
         panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
       });
+      this.dashboard.meta.autofitpanels = true;
+      console.log(this.dashboard);
     }
   }
 
diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts
index 976e4213920..23a43d80353 100644
--- a/public/app/features/dashboard/dashboard_model.ts
+++ b/public/app/features/dashboard/dashboard_model.ts
@@ -9,6 +9,7 @@ import sortByKeys from 'app/core/utils/sort_by_keys';
 
 import { PanelModel } from './panel_model';
 import { DashboardMigrator } from './dashboard_migration';
+import { tickStep } from '../../core/utils/ticks';
 
 export class DashboardModel {
   id: any;
@@ -591,6 +592,10 @@ export class DashboardModel {
 
   updateSubmenuVisibility() {
     this.meta.submenuEnabled = (() => {
+      if (this.meta.autofitpanels) {
+        return false;
+      }
+
       if (this.links.length > 0) {
         return true;
       }
diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.jest.ts
index 6ac642cd58e..adbdd37c893 100644
--- a/public/app/features/dashboard/specs/dashboard_model.jest.ts
+++ b/public/app/features/dashboard/specs/dashboard_model.jest.ts
@@ -305,6 +305,19 @@ describe('DashboardModel', function() {
     });
   });
 
+  describe('updateSubmenuVisibility with autofitpanels enabled', function() {
+    var model;
+
+    beforeEach(function() {
+      model = new DashboardModel({}, { autofitpanels: true });
+      model.updateSubmenuVisibility();
+    });
+
+    it('should not enable submmenu', function() {
+      expect(model.meta.submenuEnabled).toBe(false);
+    });
+  });
+
   describe('updateSubmenuVisibility with hidden annotation toggle', function() {
     var dashboard;
 

From f00b5eee83d606642412592fe75c348aeea7abc7 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 12:20:46 +0200
Subject: [PATCH 213/380] Remove weird import

---
 public/app/features/dashboard/dashboard_model.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts
index 23a43d80353..5a2310ac04b 100644
--- a/public/app/features/dashboard/dashboard_model.ts
+++ b/public/app/features/dashboard/dashboard_model.ts
@@ -9,7 +9,6 @@ import sortByKeys from 'app/core/utils/sort_by_keys';
 
 import { PanelModel } from './panel_model';
 import { DashboardMigrator } from './dashboard_migration';
-import { tickStep } from '../../core/utils/ticks';
 
 export class DashboardModel {
   id: any;

From 013f8cd8ea86fe94ec4856a401a81e4042b0ed7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Sun, 5 Aug 2018 11:04:12 +0200
Subject: [PATCH 214/380] refactor: moving code around a bit, refactoring PR
 #12796

---
 .../app/features/dashboard/dashboard_ctrl.ts  | 28 ++-----------------
 .../app/features/dashboard/dashboard_model.ts | 28 +++++++++++++++----
 .../dashboard/specs/dashboard_model.jest.ts   | 13 ---------
 public/app/routes/dashboard_loaders.ts        |  5 ++--
 public/sass/pages/_dashboard.scss             |  2 +-
 5 files changed, 29 insertions(+), 47 deletions(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index 9266794d6e4..cc318be4939 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -4,7 +4,6 @@ import coreModule from 'app/core/core_module';
 import { PanelContainer } from './dashgrid/PanelContainer';
 import { DashboardModel } from './dashboard_model';
 import { PanelModel } from './panel_model';
-import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
 
 export class DashboardCtrl implements PanelContainer {
   dashboard: DashboardModel;
@@ -24,8 +23,7 @@ export class DashboardCtrl implements PanelContainer {
     private unsavedChangesSrv,
     private dashboardViewStateSrv,
     public playlistSrv,
-    private panelLoader,
-    private $location
+    private panelLoader
   ) {
     // temp hack due to way dashboards are loaded
     // can't use controllerAs on route yet
@@ -64,8 +62,8 @@ export class DashboardCtrl implements PanelContainer {
       .finally(() => {
         this.dashboard = dashboard;
         this.dashboard.processRepeats();
+        this.dashboard.autoFitPanels(window.innerHeight);
 
-        this.autofitPanels();
         this.unsavedChangesSrv.init(dashboard, this.$scope);
 
         // TODO refactor ViewStateSrv
@@ -82,28 +80,6 @@ export class DashboardCtrl implements PanelContainer {
       .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true));
   }
 
-  autofitPanels() {
-    if (this.$location.search().autofitpanels) {
-      let maxRows = Math.max(
-        ...this.dashboard.panels.map(panel => {
-          return panel.gridPos.h + panel.gridPos.y;
-        })
-      );
-
-      //Consider navbar and submenu controls, padding and margin
-      let availableHeight = window.innerHeight - 40;
-      let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
-      let scaleFactor = maxRows / availableRows;
-
-      this.dashboard.panels.forEach((panel, i) => {
-        panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
-        panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
-      });
-      this.dashboard.meta.autofitpanels = true;
-      console.log(this.dashboard);
-    }
-  }
-
   onInitFailed(msg, fatal, err) {
     console.log(msg, err);
 
diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts
index 5a2310ac04b..5332357570b 100644
--- a/public/app/features/dashboard/dashboard_model.ts
+++ b/public/app/features/dashboard/dashboard_model.ts
@@ -1,7 +1,7 @@
 import moment from 'moment';
 import _ from 'lodash';
 
-import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants';
+import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
 import { DEFAULT_ANNOTATION_COLOR } from 'app/core/utils/colors';
 import { Emitter } from 'app/core/utils/emitter';
 import { contextSrv } from 'app/core/services/context_srv';
@@ -591,10 +591,6 @@ export class DashboardModel {
 
   updateSubmenuVisibility() {
     this.meta.submenuEnabled = (() => {
-      if (this.meta.autofitpanels) {
-        return false;
-      }
-
       if (this.links.length > 0) {
         return true;
       }
@@ -834,4 +830,26 @@ export class DashboardModel {
 
     return !_.isEqual(updated, this.originalTemplating);
   }
+
+  autoFitPanels(viewHeight: number) {
+    if (!this.meta.autofitpanels) {
+      return;
+    }
+
+    let maxRows = Math.max(
+      ...this.panels.map(panel => {
+        return panel.gridPos.h + panel.gridPos.y;
+      })
+    );
+
+    //Consider navbar and submenu controls, padding and margin
+    let availableHeight = window.innerHeight - 55 - 20;
+    let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
+    let scaleFactor = maxRows / availableRows;
+
+    this.panels.forEach((panel, i) => {
+      panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;
+      panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1;
+    });
+  }
 }
diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.jest.ts
index adbdd37c893..6ac642cd58e 100644
--- a/public/app/features/dashboard/specs/dashboard_model.jest.ts
+++ b/public/app/features/dashboard/specs/dashboard_model.jest.ts
@@ -305,19 +305,6 @@ describe('DashboardModel', function() {
     });
   });
 
-  describe('updateSubmenuVisibility with autofitpanels enabled', function() {
-    var model;
-
-    beforeEach(function() {
-      model = new DashboardModel({}, { autofitpanels: true });
-      model.updateSubmenuVisibility();
-    });
-
-    it('should not enable submmenu', function() {
-      expect(model.meta.submenuEnabled).toBe(false);
-    });
-  });
-
   describe('updateSubmenuVisibility with hidden annotation toggle', function() {
     var dashboard;
 
diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts
index 9224ec33bcc..3642b54c790 100644
--- a/public/app/routes/dashboard_loaders.ts
+++ b/public/app/routes/dashboard_loaders.ts
@@ -38,9 +38,10 @@ export class LoadDashboardCtrl {
         }
       }
 
-      if ($routeParams.keepRows) {
-        result.meta.keepRows = true;
+      if ($routeParams.autofitpanels) {
+        result.meta.autofitpanels = true;
       }
+
       $scope.initDashboard(result, $scope);
     });
   }
diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss
index 970b625c4f8..6225f840973 100644
--- a/public/sass/pages/_dashboard.scss
+++ b/public/sass/pages/_dashboard.scss
@@ -1,5 +1,5 @@
 .dashboard-container {
-  padding: $dashboard-padding;
+  padding: $dashboard-padding $dashboard-padding 0 $dashboard-padding;
   width: 100%;
   min-height: 100%;
 }

From b1b8a380615d508659b3000141dcaee66d31f723 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Sun, 5 Aug 2018 11:18:49 +0200
Subject: [PATCH 215/380] refactor: renaming variables, refactoring PR #12796

---
 public/app/features/dashboard/dashboard_model.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts
index 5332357570b..d82492f753b 100644
--- a/public/app/features/dashboard/dashboard_model.ts
+++ b/public/app/features/dashboard/dashboard_model.ts
@@ -836,16 +836,16 @@ export class DashboardModel {
       return;
     }
 
-    let maxRows = Math.max(
+    let currentGridHeight = Math.max(
       ...this.panels.map(panel => {
         return panel.gridPos.h + panel.gridPos.y;
       })
     );
 
     //Consider navbar and submenu controls, padding and margin
-    let availableHeight = window.innerHeight - 55 - 20;
-    let availableRows = Math.floor(availableHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
-    let scaleFactor = maxRows / availableRows;
+    let visibleHeight = window.innerHeight - 55 - 20;
+    let visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
+    let scaleFactor = currentGridHeight / visibleGridHeight;
 
     this.panels.forEach((panel, i) => {
       panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;

From 624f3a0173a46d9016f08a6959c1c00363bac80e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Sun, 5 Aug 2018 11:27:02 +0200
Subject: [PATCH 216/380] refactor: take submenu into account PR #12796

---
 public/app/features/dashboard/dashboard_ctrl.ts  |  3 +--
 public/app/features/dashboard/dashboard_model.ts | 14 ++++++++++----
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index cc318be4939..c6bb6492172 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -62,6 +62,7 @@ export class DashboardCtrl implements PanelContainer {
       .finally(() => {
         this.dashboard = dashboard;
         this.dashboard.processRepeats();
+        this.dashboard.updateSubmenuVisibility();
         this.dashboard.autoFitPanels(window.innerHeight);
 
         this.unsavedChangesSrv.init(dashboard, this.$scope);
@@ -71,8 +72,6 @@ export class DashboardCtrl implements PanelContainer {
         this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope);
 
         this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard);
-
-        this.dashboard.updateSubmenuVisibility();
         this.setWindowTitleAndTheme();
 
         this.$scope.appEvent('dashboard-initialized', dashboard);
diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts
index d82492f753b..92392fc80e8 100644
--- a/public/app/features/dashboard/dashboard_model.ts
+++ b/public/app/features/dashboard/dashboard_model.ts
@@ -836,16 +836,22 @@ export class DashboardModel {
       return;
     }
 
-    let currentGridHeight = Math.max(
+    const currentGridHeight = Math.max(
       ...this.panels.map(panel => {
         return panel.gridPos.h + panel.gridPos.y;
       })
     );
 
-    //Consider navbar and submenu controls, padding and margin
+    // Consider navbar and submenu controls, padding and margin
     let visibleHeight = window.innerHeight - 55 - 20;
-    let visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
-    let scaleFactor = currentGridHeight / visibleGridHeight;
+
+    // Remove submenu if visible
+    if (this.meta.submenuEnabled) {
+      visibleHeight -= 50;
+    }
+
+    const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
+    const scaleFactor = currentGridHeight / visibleGridHeight;
 
     this.panels.forEach((panel, i) => {
       panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1;

From 45eadae6923a903a4ff1a1709f8c8a6a30ddf7a6 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 6 Aug 2018 10:42:35 +0200
Subject: [PATCH 217/380] Convert datasource

---
 .../opentsdb/specs/datasource-specs.ts        | 105 ------------------
 .../opentsdb/specs/datasource.jest.ts         |  91 +++++++++++++++
 2 files changed, 91 insertions(+), 105 deletions(-)
 delete mode 100644 public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts
 create mode 100644 public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts

diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts b/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts
deleted file mode 100644
index a4c90af3d50..00000000000
--- a/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
-import helpers from 'test/specs/helpers';
-import OpenTsDatasource from '../datasource';
-
-describe('opentsdb', function() {
-  var ctx = new helpers.ServiceTestContext();
-  var instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } };
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(ctx.providePhase(['backendSrv']));
-
-  beforeEach(
-    angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
-      ctx.$q = $q;
-      ctx.$httpBackend = $httpBackend;
-      ctx.$rootScope = $rootScope;
-      ctx.ds = $injector.instantiate(OpenTsDatasource, {
-        instanceSettings: instanceSettings,
-      });
-      $httpBackend.when('GET', /\.html$/).respond('');
-    })
-  );
-
-  describe('When performing metricFindQuery', function() {
-    var results;
-    var requestOptions;
-
-    beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        requestOptions = options;
-        return ctx.$q.when({
-          data: [{ target: 'prod1.count', datapoints: [[10, 1], [12, 1]] }],
-        });
-      };
-    });
-
-    it('metrics() should generate api suggest query', function() {
-      ctx.ds.metricFindQuery('metrics(pew)').then(function(data) {
-        results = data;
-      });
-      ctx.$rootScope.$apply();
-      expect(requestOptions.url).to.be('/api/suggest');
-      expect(requestOptions.params.type).to.be('metrics');
-      expect(requestOptions.params.q).to.be('pew');
-      expect(results).not.to.be(null);
-    });
-
-    it('tag_names(cpu) should generate lookup query', function() {
-      ctx.ds.metricFindQuery('tag_names(cpu)').then(function(data) {
-        results = data;
-      });
-      ctx.$rootScope.$apply();
-      expect(requestOptions.url).to.be('/api/search/lookup');
-      expect(requestOptions.params.m).to.be('cpu');
-    });
-
-    it('tag_values(cpu, test) should generate lookup query', function() {
-      ctx.ds.metricFindQuery('tag_values(cpu, hostname)').then(function(data) {
-        results = data;
-      });
-      ctx.$rootScope.$apply();
-      expect(requestOptions.url).to.be('/api/search/lookup');
-      expect(requestOptions.params.m).to.be('cpu{hostname=*}');
-    });
-
-    it('tag_values(cpu, test) should generate lookup query', function() {
-      ctx.ds.metricFindQuery('tag_values(cpu, hostname, env=$env)').then(function(data) {
-        results = data;
-      });
-      ctx.$rootScope.$apply();
-      expect(requestOptions.url).to.be('/api/search/lookup');
-      expect(requestOptions.params.m).to.be('cpu{hostname=*,env=$env}');
-    });
-
-    it('tag_values(cpu, test) should generate lookup query', function() {
-      ctx.ds.metricFindQuery('tag_values(cpu, hostname, env=$env, region=$region)').then(function(data) {
-        results = data;
-      });
-      ctx.$rootScope.$apply();
-      expect(requestOptions.url).to.be('/api/search/lookup');
-      expect(requestOptions.params.m).to.be('cpu{hostname=*,env=$env,region=$region}');
-    });
-
-    it('suggest_tagk() should generate api suggest query', function() {
-      ctx.ds.metricFindQuery('suggest_tagk(foo)').then(function(data) {
-        results = data;
-      });
-      ctx.$rootScope.$apply();
-      expect(requestOptions.url).to.be('/api/suggest');
-      expect(requestOptions.params.type).to.be('tagk');
-      expect(requestOptions.params.q).to.be('foo');
-    });
-
-    it('suggest_tagv() should generate api suggest query', function() {
-      ctx.ds.metricFindQuery('suggest_tagv(bar)').then(function(data) {
-        results = data;
-      });
-      ctx.$rootScope.$apply();
-      expect(requestOptions.url).to.be('/api/suggest');
-      expect(requestOptions.params.type).to.be('tagv');
-      expect(requestOptions.params.q).to.be('bar');
-    });
-  });
-});
diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts
new file mode 100644
index 00000000000..73eca7cffde
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts
@@ -0,0 +1,91 @@
+import OpenTsDatasource from '../datasource';
+import $q from 'q';
+
+describe('opentsdb', () => {
+  let ctx = <any>{
+    backendSrv: {},
+    ds: {},
+    templateSrv: {
+      replace: str => str,
+    },
+  };
+  let instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } };
+
+  beforeEach(() => {
+    ctx.ctrl = new OpenTsDatasource(instanceSettings, $q, ctx.backendSrv, ctx.templateSrv);
+  });
+
+  describe('When performing metricFindQuery', () => {
+    var results;
+    var requestOptions;
+
+    beforeEach(async () => {
+      ctx.backendSrv.datasourceRequest = await function(options) {
+        requestOptions = options;
+        return Promise.resolve({
+          data: [{ target: 'prod1.count', datapoints: [[10, 1], [12, 1]] }],
+        });
+      };
+    });
+
+    it('metrics() should generate api suggest query', () => {
+      ctx.ctrl.metricFindQuery('metrics(pew)').then(function(data) {
+        results = data;
+      });
+      expect(requestOptions.url).toBe('/api/suggest');
+      expect(requestOptions.params.type).toBe('metrics');
+      expect(requestOptions.params.q).toBe('pew');
+      expect(results).not.toBe(null);
+    });
+
+    it('tag_names(cpu) should generate lookup query', () => {
+      ctx.ctrl.metricFindQuery('tag_names(cpu)').then(function(data) {
+        results = data;
+      });
+      expect(requestOptions.url).toBe('/api/search/lookup');
+      expect(requestOptions.params.m).toBe('cpu');
+    });
+
+    it('tag_values(cpu, test) should generate lookup query', () => {
+      ctx.ctrl.metricFindQuery('tag_values(cpu, hostname)').then(function(data) {
+        results = data;
+      });
+      expect(requestOptions.url).toBe('/api/search/lookup');
+      expect(requestOptions.params.m).toBe('cpu{hostname=*}');
+    });
+
+    it('tag_values(cpu, test) should generate lookup query', () => {
+      ctx.ctrl.metricFindQuery('tag_values(cpu, hostname, env=$env)').then(function(data) {
+        results = data;
+      });
+      expect(requestOptions.url).toBe('/api/search/lookup');
+      expect(requestOptions.params.m).toBe('cpu{hostname=*,env=$env}');
+    });
+
+    it('tag_values(cpu, test) should generate lookup query', () => {
+      ctx.ctrl.metricFindQuery('tag_values(cpu, hostname, env=$env, region=$region)').then(function(data) {
+        results = data;
+      });
+      expect(requestOptions.url).toBe('/api/search/lookup');
+      expect(requestOptions.params.m).toBe('cpu{hostname=*,env=$env,region=$region}');
+    });
+
+    it('suggest_tagk() should generate api suggest query', () => {
+      ctx.ctrl.metricFindQuery('suggest_tagk(foo)').then(function(data) {
+        results = data;
+      });
+      expect(requestOptions.url).toBe('/api/suggest');
+      expect(requestOptions.params.type).toBe('tagk');
+      expect(requestOptions.params.q).toBe('foo');
+    });
+
+    it('suggest_tagv() should generate api suggest query', () => {
+      ctx.ctrl.metricFindQuery('suggest_tagv(bar)').then(function(data) {
+        results = data;
+      });
+      expect(requestOptions.url).toBe('/api/suggest');
+      expect(requestOptions.params.type).toBe('tagv');
+      expect(requestOptions.params.q).toBe('bar');
+    });
+  });
+});

From ccd964e1dfb2e48c0c38305653b314804fc4c6b2 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 6 Aug 2018 10:57:58 +0200
Subject: [PATCH 218/380] Convert query control

---
 .../opentsdb/specs/query-ctrl-specs.ts        | 113 ------------------
 .../opentsdb/specs/query_ctrl.jest.ts         |  93 ++++++++++++++
 2 files changed, 93 insertions(+), 113 deletions(-)
 delete mode 100644 public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts
 create mode 100644 public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts

diff --git a/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts b/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts
deleted file mode 100644
index 97fc11e9d2f..00000000000
--- a/public/app/plugins/datasource/opentsdb/specs/query-ctrl-specs.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common';
-import helpers from 'test/specs/helpers';
-import { OpenTsQueryCtrl } from '../query_ctrl';
-
-describe('OpenTsQueryCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
-
-  beforeEach(ctx.providePhase(['backendSrv', 'templateSrv']));
-
-  beforeEach(ctx.providePhase());
-  beforeEach(
-    angularMocks.inject(($rootScope, $controller, $q) => {
-      ctx.$q = $q;
-      ctx.scope = $rootScope.$new();
-      ctx.target = { target: '' };
-      ctx.panelCtrl = {
-        panel: {
-          targets: [ctx.target],
-        },
-      };
-      ctx.panelCtrl.refresh = sinon.spy();
-      ctx.datasource.getAggregators = sinon.stub().returns(ctx.$q.when([]));
-      ctx.datasource.getFilterTypes = sinon.stub().returns(ctx.$q.when([]));
-
-      ctx.ctrl = $controller(
-        OpenTsQueryCtrl,
-        { $scope: ctx.scope },
-        {
-          panelCtrl: ctx.panelCtrl,
-          datasource: ctx.datasource,
-          target: ctx.target,
-        }
-      );
-      ctx.scope.$digest();
-    })
-  );
-
-  describe('init query_ctrl variables', function() {
-    it('filter types should be initialized', function() {
-      expect(ctx.ctrl.filterTypes.length).to.be(7);
-    });
-
-    it('aggregators should be initialized', function() {
-      expect(ctx.ctrl.aggregators.length).to.be(8);
-    });
-
-    it('fill policy options should be initialized', function() {
-      expect(ctx.ctrl.fillPolicies.length).to.be(4);
-    });
-  });
-
-  describe('when adding filters and tags', function() {
-    it('addTagMode should be false when closed', function() {
-      ctx.ctrl.addTagMode = true;
-      ctx.ctrl.closeAddTagMode();
-      expect(ctx.ctrl.addTagMode).to.be(false);
-    });
-
-    it('addFilterMode should be false when closed', function() {
-      ctx.ctrl.addFilterMode = true;
-      ctx.ctrl.closeAddFilterMode();
-      expect(ctx.ctrl.addFilterMode).to.be(false);
-    });
-
-    it('removing a tag from the tags list', function() {
-      ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' };
-      ctx.ctrl.removeTag('tagk');
-      expect(Object.keys(ctx.ctrl.target.tags).length).to.be(1);
-    });
-
-    it('removing a filter from the filters list', function() {
-      ctx.ctrl.target.filters = [
-        {
-          tagk: 'tag_key',
-          filter: 'tag_value2',
-          type: 'wildcard',
-          groupBy: true,
-        },
-      ];
-      ctx.ctrl.removeFilter(0);
-      expect(ctx.ctrl.target.filters.length).to.be(0);
-    });
-
-    it('adding a filter when tags exist should generate error', function() {
-      ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' };
-      ctx.ctrl.addFilter();
-      expect(ctx.ctrl.errors.filters).to.be(
-        'Please remove tags to use filters, tags and filters are mutually exclusive.'
-      );
-    });
-
-    it('adding a tag when filters exist should generate error', function() {
-      ctx.ctrl.target.filters = [
-        {
-          tagk: 'tag_key',
-          filter: 'tag_value2',
-          type: 'wildcard',
-          groupBy: true,
-        },
-      ];
-      ctx.ctrl.addTag();
-      expect(ctx.ctrl.errors.tags).to.be('Please remove filters to use tags, tags and filters are mutually exclusive.');
-    });
-  });
-});
diff --git a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts
new file mode 100644
index 00000000000..58a10b21207
--- /dev/null
+++ b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts
@@ -0,0 +1,93 @@
+import { OpenTsQueryCtrl } from '../query_ctrl';
+
+describe('OpenTsQueryCtrl', () => {
+  var ctx = <any>{
+    target: { target: '' },
+    datasource: {
+      tsdbVersion: '',
+      getAggregators: () => Promise.resolve([]),
+      getFilterTypes: () => Promise.resolve([]),
+    },
+  };
+
+  ctx.panelCtrl = {
+    panel: {
+      targets: [ctx.target],
+    },
+    refresh: () => {},
+  };
+
+  OpenTsQueryCtrl.prototype = Object.assign(OpenTsQueryCtrl.prototype, ctx);
+
+  beforeEach(() => {
+    ctx.ctrl = new OpenTsQueryCtrl({}, {});
+  });
+
+  describe('init query_ctrl variables', () => {
+    it('filter types should be initialized', () => {
+      expect(ctx.ctrl.filterTypes.length).toBe(7);
+    });
+
+    it('aggregators should be initialized', () => {
+      expect(ctx.ctrl.aggregators.length).toBe(8);
+    });
+
+    it('fill policy options should be initialized', () => {
+      expect(ctx.ctrl.fillPolicies.length).toBe(4);
+    });
+  });
+
+  describe('when adding filters and tags', () => {
+    it('addTagMode should be false when closed', () => {
+      ctx.ctrl.addTagMode = true;
+      ctx.ctrl.closeAddTagMode();
+      expect(ctx.ctrl.addTagMode).toBe(false);
+    });
+
+    it('addFilterMode should be false when closed', () => {
+      ctx.ctrl.addFilterMode = true;
+      ctx.ctrl.closeAddFilterMode();
+      expect(ctx.ctrl.addFilterMode).toBe(false);
+    });
+
+    it('removing a tag from the tags list', () => {
+      ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' };
+      ctx.ctrl.removeTag('tagk');
+      expect(Object.keys(ctx.ctrl.target.tags).length).toBe(1);
+    });
+
+    it('removing a filter from the filters list', () => {
+      ctx.ctrl.target.filters = [
+        {
+          tagk: 'tag_key',
+          filter: 'tag_value2',
+          type: 'wildcard',
+          groupBy: true,
+        },
+      ];
+      ctx.ctrl.removeFilter(0);
+      expect(ctx.ctrl.target.filters.length).toBe(0);
+    });
+
+    it('adding a filter when tags exist should generate error', () => {
+      ctx.ctrl.target.tags = { tagk: 'tag_key', tagk2: 'tag_value2' };
+      ctx.ctrl.addFilter();
+      expect(ctx.ctrl.errors.filters).toBe(
+        'Please remove tags to use filters, tags and filters are mutually exclusive.'
+      );
+    });
+
+    it('adding a tag when filters exist should generate error', () => {
+      ctx.ctrl.target.filters = [
+        {
+          tagk: 'tag_key',
+          filter: 'tag_value2',
+          type: 'wildcard',
+          groupBy: true,
+        },
+      ];
+      ctx.ctrl.addTag();
+      expect(ctx.ctrl.errors.tags).toBe('Please remove filters to use tags, tags and filters are mutually exclusive.');
+    });
+  });
+});

From 7f4723a9a7402621c6ac02ea66622db7eb2829b6 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 14:21:05 +0200
Subject: [PATCH 219/380] Begin conversion

---
 .../templating/specs/variable_srv.jest.ts     | 595 ++++++++++++++++++
 1 file changed, 595 insertions(+)
 create mode 100644 public/app/features/templating/specs/variable_srv.jest.ts

diff --git a/public/app/features/templating/specs/variable_srv.jest.ts b/public/app/features/templating/specs/variable_srv.jest.ts
new file mode 100644
index 00000000000..d38a89675e6
--- /dev/null
+++ b/public/app/features/templating/specs/variable_srv.jest.ts
@@ -0,0 +1,595 @@
+import '../all';
+import { VariableSrv } from '../variable_srv';
+import moment from 'moment';
+import $q from 'q';
+// import { model } from 'mobx-state-tree/dist/internal';
+// import { Emitter } from 'app/core/core';
+
+describe('VariableSrv', function() {
+  var ctx = <any>{
+    datasourceSrv: {},
+    timeSrv: {
+      timeRange: () => {},
+    },
+    $rootScope: {
+      $on: () => {},
+    },
+    $injector: {
+      instantiate: (ctr, obj) => new ctr(obj.model),
+    },
+    templateSrv: {
+      setGrafanaVariable: jest.fn(),
+      init: () => {},
+      updateTemplateData: () => {},
+    },
+    $location: {
+      search: () => {},
+    },
+  };
+
+  //   beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
+  //   beforeEach(
+  //     angularMocks.inject(($rootScope, $q, $location, $injector) => {
+  //       ctx.$q = $q;
+  //       ctx.$rootScope = $rootScope;
+  //       ctx.$location = $location;
+  //       ctx.variableSrv = $injector.get('variableSrv');
+  //       ctx.variableSrv.init({
+  //         templating: { list: [] },
+  //         events: new Emitter(),
+  //         updateSubmenuVisibility: sinon.stub(),
+  //       });
+  //       ctx.$rootScope.$digest();
+  //     })
+  //   );
+
+  function describeUpdateVariable(desc, fn) {
+    describe(desc, function() {
+      var scenario: any = {};
+      scenario.setup = function(setupFn) {
+        scenario.setupFn = setupFn;
+      };
+
+      beforeEach(function() {
+        scenario.setupFn();
+
+        var ds: any = {};
+        ds.metricFindQuery = Promise.resolve(scenario.queryResult);
+
+        ctx.variableSrv = new VariableSrv(ctx.$rootScope, $q, ctx.$location, ctx.$injector, ctx.templateSrv);
+
+        ctx.variableSrv.timeSrv = ctx.timeSrv;
+        console.log(ctx.variableSrv.timeSrv);
+        ctx.variableSrv.datasourceSrv = {
+          get: Promise.resolve(ds),
+          getMetricSources: () => scenario.metricSources,
+        };
+
+        ctx.variableSrv.init({
+          templating: { list: [] },
+          updateSubmenuVisibility: () => {},
+        });
+
+        scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
+        ctx.variableSrv.addVariable(scenario.variable);
+
+        ctx.variableSrv.updateOptions(scenario.variable);
+        // ctx.$rootScope.$digest();
+      });
+
+      fn(scenario);
+    });
+  }
+
+  describeUpdateVariable('interval variable without auto', scenario => {
+    scenario.setup(() => {
+      scenario.variableModel = {
+        type: 'interval',
+        query: '1s,2h,5h,1d',
+        name: 'test',
+      };
+    });
+
+    it('should update options array', () => {
+      expect(scenario.variable.options.length).toBe(4);
+      expect(scenario.variable.options[0].text).toBe('1s');
+      expect(scenario.variable.options[0].value).toBe('1s');
+    });
+  });
+
+  //
+  // Interval variable update
+  //
+  describeUpdateVariable('interval variable with auto', scenario => {
+    scenario.setup(() => {
+      scenario.variableModel = {
+        type: 'interval',
+        query: '1s,2h,5h,1d',
+        name: 'test',
+        auto: true,
+        auto_count: 10,
+      };
+
+      var range = {
+        from: moment(new Date())
+          .subtract(7, 'days')
+          .toDate(),
+        to: new Date(),
+      };
+
+      ctx.timeSrv.timeRange = () => range;
+      //   ctx.templateSrv.setGrafanaVariable = jest.fn();
+    });
+
+    it('should update options array', function() {
+      expect(scenario.variable.options.length).toBe(5);
+      expect(scenario.variable.options[0].text).toBe('auto');
+      expect(scenario.variable.options[0].value).toBe('$__auto_interval_test');
+    });
+
+    it('should set $__auto_interval_test', function() {
+      var call = ctx.templateSrv.setGrafanaVariable.firstCall;
+      expect(call.args[0]).toBe('$__auto_interval_test');
+      expect(call.args[1]).toBe('12h');
+    });
+
+    // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState()
+    // So use lastCall instead of a specific call number
+    it('should set $__auto_interval', function() {
+      var call = ctx.templateSrv.setGrafanaVariable.lastCall;
+      expect(call.args[0]).toBe('$__auto_interval');
+      expect(call.args[1]).toBe('12h');
+    });
+  });
+
+  //
+  // Query variable update
+  //
+  describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: '',
+        name: 'test',
+        current: {},
+      };
+      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
+    });
+
+    it('should set current value to first option', function() {
+      expect(scenario.variable.options.length).toBe(2);
+      expect(scenario.variable.current.value).toBe('backend1');
+    });
+  });
+
+  describeUpdateVariable(
+    'query variable with multi select and new options does not contain some selected values',
+    function(scenario) {
+      scenario.setup(function() {
+        scenario.variableModel = {
+          type: 'query',
+          query: '',
+          name: 'test',
+          current: {
+            value: ['val1', 'val2', 'val3'],
+            text: 'val1 + val2 + val3',
+          },
+        };
+        scenario.queryResult = [{ text: 'val2' }, { text: 'val3' }];
+      });
+
+      it('should update current value', function() {
+        expect(scenario.variable.current.value).toEqual(['val2', 'val3']);
+        expect(scenario.variable.current.text).toEqual('val2 + val3');
+      });
+    }
+  );
+
+  describeUpdateVariable(
+    'query variable with multi select and new options does not contain any selected values',
+    function(scenario) {
+      scenario.setup(function() {
+        scenario.variableModel = {
+          type: 'query',
+          query: '',
+          name: 'test',
+          current: {
+            value: ['val1', 'val2', 'val3'],
+            text: 'val1 + val2 + val3',
+          },
+        };
+        scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
+      });
+
+      it('should update current value with first one', function() {
+        expect(scenario.variable.current.value).toEqual('val5');
+        expect(scenario.variable.current.text).toEqual('val5');
+      });
+    }
+  );
+
+  describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: '',
+        name: 'test',
+        includeAll: true,
+        current: {
+          value: ['$__all'],
+          text: 'All',
+        },
+      };
+      scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
+    });
+
+    it('should keep current All value', function() {
+      expect(scenario.variable.current.value).toEqual(['$__all']);
+      expect(scenario.variable.current.text).toEqual('All');
+    });
+  });
+
+  describeUpdateVariable('query variable with numeric results', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: '',
+        name: 'test',
+        current: {},
+      };
+      scenario.queryResult = [{ text: 12, value: 12 }];
+    });
+
+    it('should set current value to first option', function() {
+      expect(scenario.variable.current.value).toBe('12');
+      expect(scenario.variable.options[0].value).toBe('12');
+      expect(scenario.variable.options[0].text).toBe('12');
+    });
+  });
+
+  describeUpdateVariable('basic query variable', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
+      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
+    });
+
+    it('should update options array', function() {
+      expect(scenario.variable.options.length).toBe(2);
+      expect(scenario.variable.options[0].text).toBe('backend1');
+      expect(scenario.variable.options[0].value).toBe('backend1');
+      expect(scenario.variable.options[1].value).toBe('backend2');
+    });
+
+    it('should select first option as value', function() {
+      expect(scenario.variable.current.value).toBe('backend1');
+    });
+  });
+
+  describeUpdateVariable('and existing value still exists in options', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
+      scenario.variableModel.current = { value: 'backend2', text: 'backend2' };
+      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
+    });
+
+    it('should keep variable value', function() {
+      expect(scenario.variable.current.text).toBe('backend2');
+    });
+  });
+
+  describeUpdateVariable('and regex pattern exists', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
+      scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
+      scenario.queryResult = [
+        { text: 'apps.backend.backend_01.counters.req' },
+        { text: 'apps.backend.backend_02.counters.req' },
+      ];
+    });
+
+    it('should extract and use match group', function() {
+      expect(scenario.variable.options[0].value).toBe('backend_01');
+    });
+  });
+
+  describeUpdateVariable('and regex pattern exists and no match', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
+      scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
+      scenario.queryResult = [
+        { text: 'apps.backend.backend_01.counters.req' },
+        { text: 'apps.backend.backend_02.counters.req' },
+      ];
+    });
+
+    it('should not add non matching items, None option should be added instead', function() {
+      expect(scenario.variable.options.length).toBe(1);
+      expect(scenario.variable.options[0].isNone).toBe(true);
+    });
+  });
+
+  describeUpdateVariable('regex pattern without slashes', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
+      scenario.variableModel.regex = 'backend_01';
+      scenario.queryResult = [
+        { text: 'apps.backend.backend_01.counters.req' },
+        { text: 'apps.backend.backend_02.counters.req' },
+      ];
+    });
+
+    it('should return matches options', function() {
+      expect(scenario.variable.options.length).toBe(1);
+    });
+  });
+
+  describeUpdateVariable('regex pattern remove duplicates', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
+      scenario.variableModel.regex = '/backend_01/';
+      scenario.queryResult = [
+        { text: 'apps.backend.backend_01.counters.req' },
+        { text: 'apps.backend.backend_01.counters.req' },
+      ];
+    });
+
+    it('should return matches options', function() {
+      expect(scenario.variable.options.length).toBe(1);
+    });
+  });
+
+  describeUpdateVariable('with include All', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: 'apps.*',
+        name: 'test',
+        includeAll: true,
+      };
+      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
+    });
+
+    it('should add All option', function() {
+      expect(scenario.variable.options[0].text).toBe('All');
+      expect(scenario.variable.options[0].value).toBe('$__all');
+    });
+  });
+
+  describeUpdateVariable('with include all and custom value', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: 'apps.*',
+        name: 'test',
+        includeAll: true,
+        allValue: '*',
+      };
+      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
+    });
+
+    it('should add All option with custom value', function() {
+      expect(scenario.variable.options[0].value).toBe('$__all');
+    });
+  });
+
+  describeUpdateVariable('without sort', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: 'apps.*',
+        name: 'test',
+        sort: 0,
+      };
+      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
+    });
+
+    it('should return options without sort', function() {
+      expect(scenario.variable.options[0].text).toBe('bbb2');
+      expect(scenario.variable.options[1].text).toBe('aaa10');
+      expect(scenario.variable.options[2].text).toBe('ccc3');
+    });
+  });
+
+  describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: 'apps.*',
+        name: 'test',
+        sort: 1,
+      };
+      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
+    });
+
+    it('should return options with alphabetical sort', function() {
+      expect(scenario.variable.options[0].text).toBe('aaa10');
+      expect(scenario.variable.options[1].text).toBe('bbb2');
+      expect(scenario.variable.options[2].text).toBe('ccc3');
+    });
+  });
+
+  describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: 'apps.*',
+        name: 'test',
+        sort: 2,
+      };
+      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
+    });
+
+    it('should return options with alphabetical sort', function() {
+      expect(scenario.variable.options[0].text).toBe('ccc3');
+      expect(scenario.variable.options[1].text).toBe('bbb2');
+      expect(scenario.variable.options[2].text).toBe('aaa10');
+    });
+  });
+
+  describeUpdateVariable('with numerical sort (asc)', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: 'apps.*',
+        name: 'test',
+        sort: 3,
+      };
+      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
+    });
+
+    it('should return options with numerical sort', function() {
+      expect(scenario.variable.options[0].text).toBe('bbb2');
+      expect(scenario.variable.options[1].text).toBe('ccc3');
+      expect(scenario.variable.options[2].text).toBe('aaa10');
+    });
+  });
+
+  describeUpdateVariable('with numerical sort (desc)', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'query',
+        query: 'apps.*',
+        name: 'test',
+        sort: 4,
+      };
+      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
+    });
+
+    it('should return options with numerical sort', function() {
+      expect(scenario.variable.options[0].text).toBe('aaa10');
+      expect(scenario.variable.options[1].text).toBe('ccc3');
+      expect(scenario.variable.options[2].text).toBe('bbb2');
+    });
+  });
+
+  //
+  // datasource variable update
+  //
+  describeUpdateVariable('datasource variable with regex filter', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'datasource',
+        query: 'graphite',
+        name: 'test',
+        current: { value: 'backend4_pee', text: 'backend4_pee' },
+        regex: '/pee$/',
+      };
+      scenario.metricSources = [
+        { name: 'backend1', meta: { id: 'influx' } },
+        { name: 'backend2_pee', meta: { id: 'graphite' } },
+        { name: 'backend3', meta: { id: 'graphite' } },
+        { name: 'backend4_pee', meta: { id: 'graphite' } },
+      ];
+    });
+
+    it('should set only contain graphite ds and filtered using regex', function() {
+      expect(scenario.variable.options.length).toBe(2);
+      expect(scenario.variable.options[0].value).toBe('backend2_pee');
+      expect(scenario.variable.options[1].value).toBe('backend4_pee');
+    });
+
+    it('should keep current value if available', function() {
+      expect(scenario.variable.current.value).toBe('backend4_pee');
+    });
+  });
+
+  //
+  // Custom variable update
+  //
+  describeUpdateVariable('update custom variable', function(scenario) {
+    scenario.setup(function() {
+      scenario.variableModel = {
+        type: 'custom',
+        query: 'hej, hop, asd',
+        name: 'test',
+      };
+    });
+
+    it('should update options array', function() {
+      expect(scenario.variable.options.length).toBe(3);
+      expect(scenario.variable.options[0].text).toBe('hej');
+      expect(scenario.variable.options[1].value).toBe('hop');
+    });
+  });
+
+  describe('multiple interval variables with auto', function() {
+    var variable1, variable2;
+
+    beforeEach(function() {
+      var range = {
+        from: moment(new Date())
+          .subtract(7, 'days')
+          .toDate(),
+        to: new Date(),
+      };
+      ctx.timeSrv.timeRange = () => range;
+      ctx.templateSrv.setGrafanaVariable = jest.fn();
+
+      var variableModel1 = {
+        type: 'interval',
+        query: '1s,2h,5h,1d',
+        name: 'variable1',
+        auto: true,
+        auto_count: 10,
+      };
+      variable1 = ctx.variableSrv.createVariableFromModel(variableModel1);
+      ctx.variableSrv.addVariable(variable1);
+
+      var variableModel2 = {
+        type: 'interval',
+        query: '1s,2h,5h',
+        name: 'variable2',
+        auto: true,
+        auto_count: 1000,
+      };
+      variable2 = ctx.variableSrv.createVariableFromModel(variableModel2);
+      ctx.variableSrv.addVariable(variable2);
+
+      ctx.variableSrv.updateOptions(variable1);
+      ctx.variableSrv.updateOptions(variable2);
+      ctx.$rootScope.$digest();
+    });
+
+    it('should update options array', function() {
+      expect(variable1.options.length).toBe(5);
+      expect(variable1.options[0].text).toBe('auto');
+      expect(variable1.options[0].value).toBe('$__auto_interval_variable1');
+      expect(variable2.options.length).toBe(4);
+      expect(variable2.options[0].text).toBe('auto');
+      expect(variable2.options[0].value).toBe('$__auto_interval_variable2');
+    });
+
+    it('should correctly set $__auto_interval_variableX', function() {
+      var variable1Set,
+        variable2Set,
+        legacySet,
+        unknownSet = false;
+      // updateAutoValue() gets called repeatedly: once directly once via VariableSrv.validateVariableSelectionState()
+      // So check that all calls are valid rather than expect a specific number and/or ordering of calls
+      for (var i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) {
+        var call = ctx.templateSrv.setGrafanaVariable.mock.calls[i];
+        switch (call.args[0]) {
+          case '$__auto_interval_variable1':
+            expect(call[1]).toBe('12h');
+            variable1Set = true;
+            break;
+          case '$__auto_interval_variable2':
+            expect(call[1]).toBe('10m');
+            variable2Set = true;
+            break;
+          case '$__auto_interval':
+            expect(call[1]).toEqual(expect.stringMatching(/^(12h|10m)$/));
+            legacySet = true;
+            break;
+          default:
+            unknownSet = true;
+            break;
+        }
+      }
+      expect(variable1Set).toBe.equal(true);
+      expect(variable2Set).toBe.equal(true);
+      expect(legacySet).toBe.equal(true);
+      expect(unknownSet).toBe.equal(false);
+    });
+  });
+});

From 034ca6961026c828960a7397806fe8cb1d91cdb4 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 15:17:10 +0200
Subject: [PATCH 220/380] Add mock constructor

---
 .../templating/specs/variable_srv.jest.ts     | 59 +++++++++++++------
 1 file changed, 41 insertions(+), 18 deletions(-)

diff --git a/public/app/features/templating/specs/variable_srv.jest.ts b/public/app/features/templating/specs/variable_srv.jest.ts
index d38a89675e6..33a28cda08e 100644
--- a/public/app/features/templating/specs/variable_srv.jest.ts
+++ b/public/app/features/templating/specs/variable_srv.jest.ts
@@ -19,8 +19,14 @@ describe('VariableSrv', function() {
     },
     templateSrv: {
       setGrafanaVariable: jest.fn(),
-      init: () => {},
+      init: vars => {
+        this.variables = vars;
+      },
       updateTemplateData: () => {},
+      replace: str =>
+        str.replace(this.regex, match => {
+          return match;
+        }),
     },
     $location: {
       search: () => {},
@@ -54,17 +60,20 @@ describe('VariableSrv', function() {
         scenario.setupFn();
 
         var ds: any = {};
-        ds.metricFindQuery = Promise.resolve(scenario.queryResult);
+        ds.metricFindQuery = () => Promise.resolve(scenario.queryResult);
 
         ctx.variableSrv = new VariableSrv(ctx.$rootScope, $q, ctx.$location, ctx.$injector, ctx.templateSrv);
 
         ctx.variableSrv.timeSrv = ctx.timeSrv;
-        console.log(ctx.variableSrv.timeSrv);
-        ctx.variableSrv.datasourceSrv = {
-          get: Promise.resolve(ds),
+        ctx.datasourceSrv = {
+          get: () => Promise.resolve(ds),
           getMetricSources: () => scenario.metricSources,
         };
 
+        ctx.$injector.instantiate = (ctr, model) => {
+          return getVarMockConstructor(ctr, model, ctx);
+        };
+
         ctx.variableSrv.init({
           templating: { list: [] },
           updateSubmenuVisibility: () => {},
@@ -74,7 +83,6 @@ describe('VariableSrv', function() {
         ctx.variableSrv.addVariable(scenario.variable);
 
         ctx.variableSrv.updateOptions(scenario.variable);
-        // ctx.$rootScope.$digest();
       });
 
       fn(scenario);
@@ -128,17 +136,17 @@ describe('VariableSrv', function() {
     });
 
     it('should set $__auto_interval_test', function() {
-      var call = ctx.templateSrv.setGrafanaVariable.firstCall;
-      expect(call.args[0]).toBe('$__auto_interval_test');
-      expect(call.args[1]).toBe('12h');
+      var call = ctx.templateSrv.setGrafanaVariable.mock.calls[0];
+      expect(call[0]).toBe('$__auto_interval_test');
+      expect(call[1]).toBe('12h');
     });
 
     // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState()
     // So use lastCall instead of a specific call number
     it('should set $__auto_interval', function() {
-      var call = ctx.templateSrv.setGrafanaVariable.lastCall;
-      expect(call.args[0]).toBe('$__auto_interval');
-      expect(call.args[1]).toBe('12h');
+      var call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop();
+      expect(call[0]).toBe('$__auto_interval');
+      expect(call[1]).toBe('12h');
     });
   });
 
@@ -547,7 +555,7 @@ describe('VariableSrv', function() {
 
       ctx.variableSrv.updateOptions(variable1);
       ctx.variableSrv.updateOptions(variable2);
-      ctx.$rootScope.$digest();
+      // ctx.$rootScope.$digest();
     });
 
     it('should update options array', function() {
@@ -568,7 +576,7 @@ describe('VariableSrv', function() {
       // So check that all calls are valid rather than expect a specific number and/or ordering of calls
       for (var i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) {
         var call = ctx.templateSrv.setGrafanaVariable.mock.calls[i];
-        switch (call.args[0]) {
+        switch (call[0]) {
           case '$__auto_interval_variable1':
             expect(call[1]).toBe('12h');
             variable1Set = true;
@@ -586,10 +594,25 @@ describe('VariableSrv', function() {
             break;
         }
       }
-      expect(variable1Set).toBe.equal(true);
-      expect(variable2Set).toBe.equal(true);
-      expect(legacySet).toBe.equal(true);
-      expect(unknownSet).toBe.equal(false);
+      expect(variable1Set).toEqual(true);
+      expect(variable2Set).toEqual(true);
+      expect(legacySet).toEqual(true);
+      expect(unknownSet).toEqual(false);
     });
   });
 });
+
+function getVarMockConstructor(variable, model, ctx) {
+  switch (model.model.type) {
+    case 'datasource':
+      return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv);
+    case 'query':
+      return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
+    case 'interval':
+      return new variable(model.model, ctx.timeSrv, ctx.templateSrv, ctx.variableSrv);
+    case 'custom':
+      return new variable(model.model, ctx.variableSrv);
+    default:
+      return new variable(model.model);
+  }
+}

From 46dd4eba9e9a1777bdba24b1fb4089bcec601816 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 15:38:48 +0200
Subject: [PATCH 221/380] All tests passing

---
 .../templating/specs/variable_srv.jest.ts     | 122 ++++++++----------
 1 file changed, 52 insertions(+), 70 deletions(-)

diff --git a/public/app/features/templating/specs/variable_srv.jest.ts b/public/app/features/templating/specs/variable_srv.jest.ts
index 33a28cda08e..f7796434b5e 100644
--- a/public/app/features/templating/specs/variable_srv.jest.ts
+++ b/public/app/features/templating/specs/variable_srv.jest.ts
@@ -2,8 +2,6 @@ import '../all';
 import { VariableSrv } from '../variable_srv';
 import moment from 'moment';
 import $q from 'q';
-// import { model } from 'mobx-state-tree/dist/internal';
-// import { Emitter } from 'app/core/core';
 
 describe('VariableSrv', function() {
   var ctx = <any>{
@@ -33,30 +31,14 @@ describe('VariableSrv', function() {
     },
   };
 
-  //   beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
-  //   beforeEach(
-  //     angularMocks.inject(($rootScope, $q, $location, $injector) => {
-  //       ctx.$q = $q;
-  //       ctx.$rootScope = $rootScope;
-  //       ctx.$location = $location;
-  //       ctx.variableSrv = $injector.get('variableSrv');
-  //       ctx.variableSrv.init({
-  //         templating: { list: [] },
-  //         events: new Emitter(),
-  //         updateSubmenuVisibility: sinon.stub(),
-  //       });
-  //       ctx.$rootScope.$digest();
-  //     })
-  //   );
-
   function describeUpdateVariable(desc, fn) {
-    describe(desc, function() {
+    describe(desc, () => {
       var scenario: any = {};
       scenario.setup = function(setupFn) {
         scenario.setupFn = setupFn;
       };
 
-      beforeEach(function() {
+      beforeEach(async () => {
         scenario.setupFn();
 
         var ds: any = {};
@@ -82,7 +64,7 @@ describe('VariableSrv', function() {
         scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
         ctx.variableSrv.addVariable(scenario.variable);
 
-        ctx.variableSrv.updateOptions(scenario.variable);
+        await ctx.variableSrv.updateOptions(scenario.variable);
       });
 
       fn(scenario);
@@ -129,13 +111,13 @@ describe('VariableSrv', function() {
       //   ctx.templateSrv.setGrafanaVariable = jest.fn();
     });
 
-    it('should update options array', function() {
+    it('should update options array', () => {
       expect(scenario.variable.options.length).toBe(5);
       expect(scenario.variable.options[0].text).toBe('auto');
       expect(scenario.variable.options[0].value).toBe('$__auto_interval_test');
     });
 
-    it('should set $__auto_interval_test', function() {
+    it('should set $__auto_interval_test', () => {
       var call = ctx.templateSrv.setGrafanaVariable.mock.calls[0];
       expect(call[0]).toBe('$__auto_interval_test');
       expect(call[1]).toBe('12h');
@@ -143,7 +125,7 @@ describe('VariableSrv', function() {
 
     // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState()
     // So use lastCall instead of a specific call number
-    it('should set $__auto_interval', function() {
+    it('should set $__auto_interval', () => {
       var call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop();
       expect(call[0]).toBe('$__auto_interval');
       expect(call[1]).toBe('12h');
@@ -154,7 +136,7 @@ describe('VariableSrv', function() {
   // Query variable update
   //
   describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: '',
@@ -164,7 +146,7 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
     });
 
-    it('should set current value to first option', function() {
+    it('should set current value to first option', () => {
       expect(scenario.variable.options.length).toBe(2);
       expect(scenario.variable.current.value).toBe('backend1');
     });
@@ -173,7 +155,7 @@ describe('VariableSrv', function() {
   describeUpdateVariable(
     'query variable with multi select and new options does not contain some selected values',
     function(scenario) {
-      scenario.setup(function() {
+      scenario.setup(() => {
         scenario.variableModel = {
           type: 'query',
           query: '',
@@ -186,7 +168,7 @@ describe('VariableSrv', function() {
         scenario.queryResult = [{ text: 'val2' }, { text: 'val3' }];
       });
 
-      it('should update current value', function() {
+      it('should update current value', () => {
         expect(scenario.variable.current.value).toEqual(['val2', 'val3']);
         expect(scenario.variable.current.text).toEqual('val2 + val3');
       });
@@ -196,7 +178,7 @@ describe('VariableSrv', function() {
   describeUpdateVariable(
     'query variable with multi select and new options does not contain any selected values',
     function(scenario) {
-      scenario.setup(function() {
+      scenario.setup(() => {
         scenario.variableModel = {
           type: 'query',
           query: '',
@@ -209,7 +191,7 @@ describe('VariableSrv', function() {
         scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
       });
 
-      it('should update current value with first one', function() {
+      it('should update current value with first one', () => {
         expect(scenario.variable.current.value).toEqual('val5');
         expect(scenario.variable.current.text).toEqual('val5');
       });
@@ -217,7 +199,7 @@ describe('VariableSrv', function() {
   );
 
   describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: '',
@@ -231,14 +213,14 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
     });
 
-    it('should keep current All value', function() {
+    it('should keep current All value', () => {
       expect(scenario.variable.current.value).toEqual(['$__all']);
       expect(scenario.variable.current.text).toEqual('All');
     });
   });
 
   describeUpdateVariable('query variable with numeric results', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: '',
@@ -248,7 +230,7 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 12, value: 12 }];
     });
 
-    it('should set current value to first option', function() {
+    it('should set current value to first option', () => {
       expect(scenario.variable.current.value).toBe('12');
       expect(scenario.variable.options[0].value).toBe('12');
       expect(scenario.variable.options[0].text).toBe('12');
@@ -256,37 +238,37 @@ describe('VariableSrv', function() {
   });
 
   describeUpdateVariable('basic query variable', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
       scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
     });
 
-    it('should update options array', function() {
+    it('should update options array', () => {
       expect(scenario.variable.options.length).toBe(2);
       expect(scenario.variable.options[0].text).toBe('backend1');
       expect(scenario.variable.options[0].value).toBe('backend1');
       expect(scenario.variable.options[1].value).toBe('backend2');
     });
 
-    it('should select first option as value', function() {
+    it('should select first option as value', () => {
       expect(scenario.variable.current.value).toBe('backend1');
     });
   });
 
   describeUpdateVariable('and existing value still exists in options', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
       scenario.variableModel.current = { value: 'backend2', text: 'backend2' };
       scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
     });
 
-    it('should keep variable value', function() {
+    it('should keep variable value', () => {
       expect(scenario.variable.current.text).toBe('backend2');
     });
   });
 
   describeUpdateVariable('and regex pattern exists', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
       scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
       scenario.queryResult = [
@@ -295,13 +277,13 @@ describe('VariableSrv', function() {
       ];
     });
 
-    it('should extract and use match group', function() {
+    it('should extract and use match group', () => {
       expect(scenario.variable.options[0].value).toBe('backend_01');
     });
   });
 
   describeUpdateVariable('and regex pattern exists and no match', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
       scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
       scenario.queryResult = [
@@ -310,14 +292,14 @@ describe('VariableSrv', function() {
       ];
     });
 
-    it('should not add non matching items, None option should be added instead', function() {
+    it('should not add non matching items, None option should be added instead', () => {
       expect(scenario.variable.options.length).toBe(1);
       expect(scenario.variable.options[0].isNone).toBe(true);
     });
   });
 
   describeUpdateVariable('regex pattern without slashes', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
       scenario.variableModel.regex = 'backend_01';
       scenario.queryResult = [
@@ -326,13 +308,13 @@ describe('VariableSrv', function() {
       ];
     });
 
-    it('should return matches options', function() {
+    it('should return matches options', () => {
       expect(scenario.variable.options.length).toBe(1);
     });
   });
 
   describeUpdateVariable('regex pattern remove duplicates', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
       scenario.variableModel.regex = '/backend_01/';
       scenario.queryResult = [
@@ -341,13 +323,13 @@ describe('VariableSrv', function() {
       ];
     });
 
-    it('should return matches options', function() {
+    it('should return matches options', () => {
       expect(scenario.variable.options.length).toBe(1);
     });
   });
 
   describeUpdateVariable('with include All', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: 'apps.*',
@@ -357,14 +339,14 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
     });
 
-    it('should add All option', function() {
+    it('should add All option', () => {
       expect(scenario.variable.options[0].text).toBe('All');
       expect(scenario.variable.options[0].value).toBe('$__all');
     });
   });
 
   describeUpdateVariable('with include all and custom value', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: 'apps.*',
@@ -375,13 +357,13 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
     });
 
-    it('should add All option with custom value', function() {
+    it('should add All option with custom value', () => {
       expect(scenario.variable.options[0].value).toBe('$__all');
     });
   });
 
   describeUpdateVariable('without sort', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: 'apps.*',
@@ -391,7 +373,7 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
     });
 
-    it('should return options without sort', function() {
+    it('should return options without sort', () => {
       expect(scenario.variable.options[0].text).toBe('bbb2');
       expect(scenario.variable.options[1].text).toBe('aaa10');
       expect(scenario.variable.options[2].text).toBe('ccc3');
@@ -399,7 +381,7 @@ describe('VariableSrv', function() {
   });
 
   describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: 'apps.*',
@@ -409,7 +391,7 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
     });
 
-    it('should return options with alphabetical sort', function() {
+    it('should return options with alphabetical sort', () => {
       expect(scenario.variable.options[0].text).toBe('aaa10');
       expect(scenario.variable.options[1].text).toBe('bbb2');
       expect(scenario.variable.options[2].text).toBe('ccc3');
@@ -417,7 +399,7 @@ describe('VariableSrv', function() {
   });
 
   describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: 'apps.*',
@@ -427,7 +409,7 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
     });
 
-    it('should return options with alphabetical sort', function() {
+    it('should return options with alphabetical sort', () => {
       expect(scenario.variable.options[0].text).toBe('ccc3');
       expect(scenario.variable.options[1].text).toBe('bbb2');
       expect(scenario.variable.options[2].text).toBe('aaa10');
@@ -435,7 +417,7 @@ describe('VariableSrv', function() {
   });
 
   describeUpdateVariable('with numerical sort (asc)', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: 'apps.*',
@@ -445,7 +427,7 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
     });
 
-    it('should return options with numerical sort', function() {
+    it('should return options with numerical sort', () => {
       expect(scenario.variable.options[0].text).toBe('bbb2');
       expect(scenario.variable.options[1].text).toBe('ccc3');
       expect(scenario.variable.options[2].text).toBe('aaa10');
@@ -453,7 +435,7 @@ describe('VariableSrv', function() {
   });
 
   describeUpdateVariable('with numerical sort (desc)', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'query',
         query: 'apps.*',
@@ -463,7 +445,7 @@ describe('VariableSrv', function() {
       scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
     });
 
-    it('should return options with numerical sort', function() {
+    it('should return options with numerical sort', () => {
       expect(scenario.variable.options[0].text).toBe('aaa10');
       expect(scenario.variable.options[1].text).toBe('ccc3');
       expect(scenario.variable.options[2].text).toBe('bbb2');
@@ -474,7 +456,7 @@ describe('VariableSrv', function() {
   // datasource variable update
   //
   describeUpdateVariable('datasource variable with regex filter', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'datasource',
         query: 'graphite',
@@ -490,13 +472,13 @@ describe('VariableSrv', function() {
       ];
     });
 
-    it('should set only contain graphite ds and filtered using regex', function() {
+    it('should set only contain graphite ds and filtered using regex', () => {
       expect(scenario.variable.options.length).toBe(2);
       expect(scenario.variable.options[0].value).toBe('backend2_pee');
       expect(scenario.variable.options[1].value).toBe('backend4_pee');
     });
 
-    it('should keep current value if available', function() {
+    it('should keep current value if available', () => {
       expect(scenario.variable.current.value).toBe('backend4_pee');
     });
   });
@@ -505,7 +487,7 @@ describe('VariableSrv', function() {
   // Custom variable update
   //
   describeUpdateVariable('update custom variable', function(scenario) {
-    scenario.setup(function() {
+    scenario.setup(() => {
       scenario.variableModel = {
         type: 'custom',
         query: 'hej, hop, asd',
@@ -513,17 +495,17 @@ describe('VariableSrv', function() {
       };
     });
 
-    it('should update options array', function() {
+    it('should update options array', () => {
       expect(scenario.variable.options.length).toBe(3);
       expect(scenario.variable.options[0].text).toBe('hej');
       expect(scenario.variable.options[1].value).toBe('hop');
     });
   });
 
-  describe('multiple interval variables with auto', function() {
+  describe('multiple interval variables with auto', () => {
     var variable1, variable2;
 
-    beforeEach(function() {
+    beforeEach(() => {
       var range = {
         from: moment(new Date())
           .subtract(7, 'days')
@@ -558,7 +540,7 @@ describe('VariableSrv', function() {
       // ctx.$rootScope.$digest();
     });
 
-    it('should update options array', function() {
+    it('should update options array', () => {
       expect(variable1.options.length).toBe(5);
       expect(variable1.options[0].text).toBe('auto');
       expect(variable1.options[0].value).toBe('$__auto_interval_variable1');
@@ -567,7 +549,7 @@ describe('VariableSrv', function() {
       expect(variable2.options[0].value).toBe('$__auto_interval_variable2');
     });
 
-    it('should correctly set $__auto_interval_variableX', function() {
+    it('should correctly set $__auto_interval_variableX', () => {
       var variable1Set,
         variable2Set,
         legacySet,

From 9f87f6081af9945ea2aa1099c897bc6458c5f42d Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 15:39:59 +0200
Subject: [PATCH 222/380] Remove Karma test

---
 .../templating/specs/variable_srv_specs.ts    | 568 ------------------
 1 file changed, 568 deletions(-)
 delete mode 100644 public/app/features/templating/specs/variable_srv_specs.ts

diff --git a/public/app/features/templating/specs/variable_srv_specs.ts b/public/app/features/templating/specs/variable_srv_specs.ts
deleted file mode 100644
index 6ab5dcad20e..00000000000
--- a/public/app/features/templating/specs/variable_srv_specs.ts
+++ /dev/null
@@ -1,568 +0,0 @@
-import { describe, beforeEach, it, sinon, expect, angularMocks } from 'test/lib/common';
-
-import '../all';
-
-import moment from 'moment';
-import helpers from 'test/specs/helpers';
-import { Emitter } from 'app/core/core';
-
-describe('VariableSrv', function() {
-  var ctx = new helpers.ControllerTestContext();
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(angularMocks.module('grafana.services'));
-
-  beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', '$location']));
-  beforeEach(
-    angularMocks.inject(($rootScope, $q, $location, $injector) => {
-      ctx.$q = $q;
-      ctx.$rootScope = $rootScope;
-      ctx.$location = $location;
-      ctx.variableSrv = $injector.get('variableSrv');
-      ctx.variableSrv.init({
-        templating: { list: [] },
-        events: new Emitter(),
-        updateSubmenuVisibility: sinon.stub(),
-      });
-      ctx.$rootScope.$digest();
-    })
-  );
-
-  function describeUpdateVariable(desc, fn) {
-    describe(desc, function() {
-      var scenario: any = {};
-      scenario.setup = function(setupFn) {
-        scenario.setupFn = setupFn;
-      };
-
-      beforeEach(function() {
-        scenario.setupFn();
-        var ds: any = {};
-        ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult));
-        ctx.datasourceSrv.get = sinon.stub().returns(ctx.$q.when(ds));
-        ctx.datasourceSrv.getMetricSources = sinon.stub().returns(scenario.metricSources);
-
-        scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
-        ctx.variableSrv.addVariable(scenario.variable);
-
-        ctx.variableSrv.updateOptions(scenario.variable);
-        ctx.$rootScope.$digest();
-      });
-
-      fn(scenario);
-    });
-  }
-
-  describeUpdateVariable('interval variable without auto', scenario => {
-    scenario.setup(() => {
-      scenario.variableModel = {
-        type: 'interval',
-        query: '1s,2h,5h,1d',
-        name: 'test',
-      };
-    });
-
-    it('should update options array', () => {
-      expect(scenario.variable.options.length).to.be(4);
-      expect(scenario.variable.options[0].text).to.be('1s');
-      expect(scenario.variable.options[0].value).to.be('1s');
-    });
-  });
-
-  //
-  // Interval variable update
-  //
-  describeUpdateVariable('interval variable with auto', scenario => {
-    scenario.setup(() => {
-      scenario.variableModel = {
-        type: 'interval',
-        query: '1s,2h,5h,1d',
-        name: 'test',
-        auto: true,
-        auto_count: 10,
-      };
-
-      var range = {
-        from: moment(new Date())
-          .subtract(7, 'days')
-          .toDate(),
-        to: new Date(),
-      };
-
-      ctx.timeSrv.timeRange = sinon.stub().returns(range);
-      ctx.templateSrv.setGrafanaVariable = sinon.spy();
-    });
-
-    it('should update options array', function() {
-      expect(scenario.variable.options.length).to.be(5);
-      expect(scenario.variable.options[0].text).to.be('auto');
-      expect(scenario.variable.options[0].value).to.be('$__auto_interval_test');
-    });
-
-    it('should set $__auto_interval_test', function() {
-      var call = ctx.templateSrv.setGrafanaVariable.firstCall;
-      expect(call.args[0]).to.be('$__auto_interval_test');
-      expect(call.args[1]).to.be('12h');
-    });
-
-    // updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState()
-    // So use lastCall instead of a specific call number
-    it('should set $__auto_interval', function() {
-      var call = ctx.templateSrv.setGrafanaVariable.lastCall;
-      expect(call.args[0]).to.be('$__auto_interval');
-      expect(call.args[1]).to.be('12h');
-    });
-  });
-
-  //
-  // Query variable update
-  //
-  describeUpdateVariable('query variable with empty current object and refresh', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: '',
-        name: 'test',
-        current: {},
-      };
-      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
-    });
-
-    it('should set current value to first option', function() {
-      expect(scenario.variable.options.length).to.be(2);
-      expect(scenario.variable.current.value).to.be('backend1');
-    });
-  });
-
-  describeUpdateVariable(
-    'query variable with multi select and new options does not contain some selected values',
-    function(scenario) {
-      scenario.setup(function() {
-        scenario.variableModel = {
-          type: 'query',
-          query: '',
-          name: 'test',
-          current: {
-            value: ['val1', 'val2', 'val3'],
-            text: 'val1 + val2 + val3',
-          },
-        };
-        scenario.queryResult = [{ text: 'val2' }, { text: 'val3' }];
-      });
-
-      it('should update current value', function() {
-        expect(scenario.variable.current.value).to.eql(['val2', 'val3']);
-        expect(scenario.variable.current.text).to.eql('val2 + val3');
-      });
-    }
-  );
-
-  describeUpdateVariable(
-    'query variable with multi select and new options does not contain any selected values',
-    function(scenario) {
-      scenario.setup(function() {
-        scenario.variableModel = {
-          type: 'query',
-          query: '',
-          name: 'test',
-          current: {
-            value: ['val1', 'val2', 'val3'],
-            text: 'val1 + val2 + val3',
-          },
-        };
-        scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
-      });
-
-      it('should update current value with first one', function() {
-        expect(scenario.variable.current.value).to.eql('val5');
-        expect(scenario.variable.current.text).to.eql('val5');
-      });
-    }
-  );
-
-  describeUpdateVariable('query variable with multi select and $__all selected', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: '',
-        name: 'test',
-        includeAll: true,
-        current: {
-          value: ['$__all'],
-          text: 'All',
-        },
-      };
-      scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
-    });
-
-    it('should keep current All value', function() {
-      expect(scenario.variable.current.value).to.eql(['$__all']);
-      expect(scenario.variable.current.text).to.eql('All');
-    });
-  });
-
-  describeUpdateVariable('query variable with numeric results', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: '',
-        name: 'test',
-        current: {},
-      };
-      scenario.queryResult = [{ text: 12, value: 12 }];
-    });
-
-    it('should set current value to first option', function() {
-      expect(scenario.variable.current.value).to.be('12');
-      expect(scenario.variable.options[0].value).to.be('12');
-      expect(scenario.variable.options[0].text).to.be('12');
-    });
-  });
-
-  describeUpdateVariable('basic query variable', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
-      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
-    });
-
-    it('should update options array', function() {
-      expect(scenario.variable.options.length).to.be(2);
-      expect(scenario.variable.options[0].text).to.be('backend1');
-      expect(scenario.variable.options[0].value).to.be('backend1');
-      expect(scenario.variable.options[1].value).to.be('backend2');
-    });
-
-    it('should select first option as value', function() {
-      expect(scenario.variable.current.value).to.be('backend1');
-    });
-  });
-
-  describeUpdateVariable('and existing value still exists in options', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
-      scenario.variableModel.current = { value: 'backend2', text: 'backend2' };
-      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
-    });
-
-    it('should keep variable value', function() {
-      expect(scenario.variable.current.text).to.be('backend2');
-    });
-  });
-
-  describeUpdateVariable('and regex pattern exists', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
-      scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
-      scenario.queryResult = [
-        { text: 'apps.backend.backend_01.counters.req' },
-        { text: 'apps.backend.backend_02.counters.req' },
-      ];
-    });
-
-    it('should extract and use match group', function() {
-      expect(scenario.variable.options[0].value).to.be('backend_01');
-    });
-  });
-
-  describeUpdateVariable('and regex pattern exists and no match', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
-      scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
-      scenario.queryResult = [
-        { text: 'apps.backend.backend_01.counters.req' },
-        { text: 'apps.backend.backend_02.counters.req' },
-      ];
-    });
-
-    it('should not add non matching items, None option should be added instead', function() {
-      expect(scenario.variable.options.length).to.be(1);
-      expect(scenario.variable.options[0].isNone).to.be(true);
-    });
-  });
-
-  describeUpdateVariable('regex pattern without slashes', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
-      scenario.variableModel.regex = 'backend_01';
-      scenario.queryResult = [
-        { text: 'apps.backend.backend_01.counters.req' },
-        { text: 'apps.backend.backend_02.counters.req' },
-      ];
-    });
-
-    it('should return matches options', function() {
-      expect(scenario.variable.options.length).to.be(1);
-    });
-  });
-
-  describeUpdateVariable('regex pattern remove duplicates', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
-      scenario.variableModel.regex = '/backend_01/';
-      scenario.queryResult = [
-        { text: 'apps.backend.backend_01.counters.req' },
-        { text: 'apps.backend.backend_01.counters.req' },
-      ];
-    });
-
-    it('should return matches options', function() {
-      expect(scenario.variable.options.length).to.be(1);
-    });
-  });
-
-  describeUpdateVariable('with include All', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: 'apps.*',
-        name: 'test',
-        includeAll: true,
-      };
-      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
-    });
-
-    it('should add All option', function() {
-      expect(scenario.variable.options[0].text).to.be('All');
-      expect(scenario.variable.options[0].value).to.be('$__all');
-    });
-  });
-
-  describeUpdateVariable('with include all and custom value', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: 'apps.*',
-        name: 'test',
-        includeAll: true,
-        allValue: '*',
-      };
-      scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
-    });
-
-    it('should add All option with custom value', function() {
-      expect(scenario.variable.options[0].value).to.be('$__all');
-    });
-  });
-
-  describeUpdateVariable('without sort', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: 'apps.*',
-        name: 'test',
-        sort: 0,
-      };
-      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
-    });
-
-    it('should return options without sort', function() {
-      expect(scenario.variable.options[0].text).to.be('bbb2');
-      expect(scenario.variable.options[1].text).to.be('aaa10');
-      expect(scenario.variable.options[2].text).to.be('ccc3');
-    });
-  });
-
-  describeUpdateVariable('with alphabetical sort (asc)', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: 'apps.*',
-        name: 'test',
-        sort: 1,
-      };
-      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
-    });
-
-    it('should return options with alphabetical sort', function() {
-      expect(scenario.variable.options[0].text).to.be('aaa10');
-      expect(scenario.variable.options[1].text).to.be('bbb2');
-      expect(scenario.variable.options[2].text).to.be('ccc3');
-    });
-  });
-
-  describeUpdateVariable('with alphabetical sort (desc)', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: 'apps.*',
-        name: 'test',
-        sort: 2,
-      };
-      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
-    });
-
-    it('should return options with alphabetical sort', function() {
-      expect(scenario.variable.options[0].text).to.be('ccc3');
-      expect(scenario.variable.options[1].text).to.be('bbb2');
-      expect(scenario.variable.options[2].text).to.be('aaa10');
-    });
-  });
-
-  describeUpdateVariable('with numerical sort (asc)', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: 'apps.*',
-        name: 'test',
-        sort: 3,
-      };
-      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
-    });
-
-    it('should return options with numerical sort', function() {
-      expect(scenario.variable.options[0].text).to.be('bbb2');
-      expect(scenario.variable.options[1].text).to.be('ccc3');
-      expect(scenario.variable.options[2].text).to.be('aaa10');
-    });
-  });
-
-  describeUpdateVariable('with numerical sort (desc)', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'query',
-        query: 'apps.*',
-        name: 'test',
-        sort: 4,
-      };
-      scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
-    });
-
-    it('should return options with numerical sort', function() {
-      expect(scenario.variable.options[0].text).to.be('aaa10');
-      expect(scenario.variable.options[1].text).to.be('ccc3');
-      expect(scenario.variable.options[2].text).to.be('bbb2');
-    });
-  });
-
-  //
-  // datasource variable update
-  //
-  describeUpdateVariable('datasource variable with regex filter', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'datasource',
-        query: 'graphite',
-        name: 'test',
-        current: { value: 'backend4_pee', text: 'backend4_pee' },
-        regex: '/pee$/',
-      };
-      scenario.metricSources = [
-        { name: 'backend1', meta: { id: 'influx' } },
-        { name: 'backend2_pee', meta: { id: 'graphite' } },
-        { name: 'backend3', meta: { id: 'graphite' } },
-        { name: 'backend4_pee', meta: { id: 'graphite' } },
-      ];
-    });
-
-    it('should set only contain graphite ds and filtered using regex', function() {
-      expect(scenario.variable.options.length).to.be(2);
-      expect(scenario.variable.options[0].value).to.be('backend2_pee');
-      expect(scenario.variable.options[1].value).to.be('backend4_pee');
-    });
-
-    it('should keep current value if available', function() {
-      expect(scenario.variable.current.value).to.be('backend4_pee');
-    });
-  });
-
-  //
-  // Custom variable update
-  //
-  describeUpdateVariable('update custom variable', function(scenario) {
-    scenario.setup(function() {
-      scenario.variableModel = {
-        type: 'custom',
-        query: 'hej, hop, asd',
-        name: 'test',
-      };
-    });
-
-    it('should update options array', function() {
-      expect(scenario.variable.options.length).to.be(3);
-      expect(scenario.variable.options[0].text).to.be('hej');
-      expect(scenario.variable.options[1].value).to.be('hop');
-    });
-  });
-
-  describe('multiple interval variables with auto', function() {
-    var variable1, variable2;
-
-    beforeEach(function() {
-      var range = {
-        from: moment(new Date())
-          .subtract(7, 'days')
-          .toDate(),
-        to: new Date(),
-      };
-      ctx.timeSrv.timeRange = sinon.stub().returns(range);
-      ctx.templateSrv.setGrafanaVariable = sinon.spy();
-
-      var variableModel1 = {
-        type: 'interval',
-        query: '1s,2h,5h,1d',
-        name: 'variable1',
-        auto: true,
-        auto_count: 10,
-      };
-      variable1 = ctx.variableSrv.createVariableFromModel(variableModel1);
-      ctx.variableSrv.addVariable(variable1);
-
-      var variableModel2 = {
-        type: 'interval',
-        query: '1s,2h,5h',
-        name: 'variable2',
-        auto: true,
-        auto_count: 1000,
-      };
-      variable2 = ctx.variableSrv.createVariableFromModel(variableModel2);
-      ctx.variableSrv.addVariable(variable2);
-
-      ctx.variableSrv.updateOptions(variable1);
-      ctx.variableSrv.updateOptions(variable2);
-      ctx.$rootScope.$digest();
-    });
-
-    it('should update options array', function() {
-      expect(variable1.options.length).to.be(5);
-      expect(variable1.options[0].text).to.be('auto');
-      expect(variable1.options[0].value).to.be('$__auto_interval_variable1');
-      expect(variable2.options.length).to.be(4);
-      expect(variable2.options[0].text).to.be('auto');
-      expect(variable2.options[0].value).to.be('$__auto_interval_variable2');
-    });
-
-    it('should correctly set $__auto_interval_variableX', function() {
-      var variable1Set,
-        variable2Set,
-        legacySet,
-        unknownSet = false;
-      // updateAutoValue() gets called repeatedly: once directly once via VariableSrv.validateVariableSelectionState()
-      // So check that all calls are valid rather than expect a specific number and/or ordering of calls
-      for (var i = 0; i < ctx.templateSrv.setGrafanaVariable.callCount; i++) {
-        var call = ctx.templateSrv.setGrafanaVariable.getCall(i);
-        switch (call.args[0]) {
-          case '$__auto_interval_variable1':
-            expect(call.args[1]).to.be('12h');
-            variable1Set = true;
-            break;
-          case '$__auto_interval_variable2':
-            expect(call.args[1]).to.be('10m');
-            variable2Set = true;
-            break;
-          case '$__auto_interval':
-            expect(call.args[1]).to.match(/^(12h|10m)$/);
-            legacySet = true;
-            break;
-          default:
-            unknownSet = true;
-            break;
-        }
-      }
-      expect(variable1Set).to.be.equal(true);
-      expect(variable2Set).to.be.equal(true);
-      expect(legacySet).to.be.equal(true);
-      expect(unknownSet).to.be.equal(false);
-    });
-  });
-});

From 3096905d393b860f63c117c7a2e1dcd5a000f088 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 6 Aug 2018 14:04:41 +0200
Subject: [PATCH 223/380] docs: how to build a docker image.

---
 README.md | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/README.md b/README.md
index 322523d703b..b2baf0ece59 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,15 @@ bra run
 
 Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
 
+### Building a docker image (on linux/amd64)
+
+This builds a docker image from your local sources:
+
+1. Build the frontend `go run build.go build-frontend`
+2. Build the docker image `make build-docker-dev`
+
+The resulting image will be tagged as `grafana/grafana:dev`
+
 ### Dev config
 
 Create a custom.ini in the conf directory to override default configuration options.

From 5da3584dd4fee9a681ee0c599bd551849770cd46 Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Mon, 6 Aug 2018 14:36:02 +0200
Subject: [PATCH 224/380] Explore: facetting for label completion (#12786)

* Explore: facetting for label completion

- unified metric and non-metric label completion
- label keys and values are now fetched fresh for each valid selector
- complete selector means only values are suggested that are supported
  by the selector
- properly implemented metric lookup for selectors (until the first
  metric was used which breaks when multiple metrics are present)
- typeahead tests now need a valid selection to demark the cursor

* Fix facetting queries for empty selector
---
 .../Explore/PromQueryField.jest.tsx           |  92 +++++++++---
 .../app/containers/Explore/PromQueryField.tsx | 136 +++++++++---------
 public/app/containers/Explore/QueryField.tsx  |   3 +
 .../Explore/utils/prometheus.jest.ts          |  33 +++++
 .../containers/Explore/utils/prometheus.ts    |  70 ++++++++-
 5 files changed, 248 insertions(+), 86 deletions(-)
 create mode 100644 public/app/containers/Explore/utils/prometheus.jest.ts

diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx
index 8d2903cb2c2..cd0d940961e 100644
--- a/public/app/containers/Explore/PromQueryField.jest.tsx
+++ b/public/app/containers/Explore/PromQueryField.jest.tsx
@@ -1,11 +1,12 @@
 import React from 'react';
 import Enzyme, { shallow } from 'enzyme';
 import Adapter from 'enzyme-adapter-react-16';
-
-Enzyme.configure({ adapter: new Adapter() });
+import Plain from 'slate-plain-serializer';
 
 import PromQueryField from './PromQueryField';
 
+Enzyme.configure({ adapter: new Adapter() });
+
 describe('PromQueryField typeahead handling', () => {
   const defaultProps = {
     request: () => ({ data: { data: [] } }),
@@ -59,20 +60,35 @@ describe('PromQueryField typeahead handling', () => {
   describe('label suggestions', () => {
     it('returns default label suggestions on label context and no metric', () => {
       const instance = shallow(<PromQueryField {...defaultProps} />).instance() as PromQueryField;
-      const result = instance.getTypeahead({ text: 'j', prefix: 'j', wrapperClasses: ['context-labels'] });
+      const value = Plain.deserialize('{}');
+      const range = value.selection.merge({
+        anchorOffset: 1,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.getTypeahead({
+        text: '',
+        prefix: '',
+        wrapperClasses: ['context-labels'],
+        value: valueWithSelection,
+      });
       expect(result.context).toBe('context-labels');
       expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
     });
 
     it('returns label suggestions on label context and metric', () => {
       const instance = shallow(
-        <PromQueryField {...defaultProps} labelKeys={{ foo: ['bar'] }} />
+        <PromQueryField {...defaultProps} labelKeys={{ '{__name__="metric"}': ['bar'] }} />
       ).instance() as PromQueryField;
+      const value = Plain.deserialize('metric{}');
+      const range = value.selection.merge({
+        anchorOffset: 7,
+      });
+      const valueWithSelection = value.change().select(range).value;
       const result = instance.getTypeahead({
-        text: 'job',
-        prefix: 'job',
+        text: '',
+        prefix: '',
         wrapperClasses: ['context-labels'],
-        metric: 'foo',
+        value: valueWithSelection,
       });
       expect(result.context).toBe('context-labels');
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
@@ -80,13 +96,18 @@ describe('PromQueryField typeahead handling', () => {
 
     it('returns a refresher on label context and unavailable metric', () => {
       const instance = shallow(
-        <PromQueryField {...defaultProps} labelKeys={{ foo: ['bar'] }} />
+        <PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />
       ).instance() as PromQueryField;
+      const value = Plain.deserialize('metric{}');
+      const range = value.selection.merge({
+        anchorOffset: 7,
+      });
+      const valueWithSelection = value.change().select(range).value;
       const result = instance.getTypeahead({
-        text: 'job',
-        prefix: 'job',
+        text: '',
+        prefix: '',
         wrapperClasses: ['context-labels'],
-        metric: 'xxx',
+        value: valueWithSelection,
       });
       expect(result.context).toBeUndefined();
       expect(result.refresher).toBeInstanceOf(Promise);
@@ -95,28 +116,61 @@ describe('PromQueryField typeahead handling', () => {
 
     it('returns label values on label context when given a metric and a label key', () => {
       const instance = shallow(
-        <PromQueryField {...defaultProps} labelKeys={{ foo: ['bar'] }} labelValues={{ foo: { bar: ['baz'] } }} />
+        <PromQueryField
+          {...defaultProps}
+          labelKeys={{ '{__name__="metric"}': ['bar'] }}
+          labelValues={{ '{__name__="metric"}': { bar: ['baz'] } }}
+        />
       ).instance() as PromQueryField;
+      const value = Plain.deserialize('metric{bar=ba}');
+      const range = value.selection.merge({
+        anchorOffset: 13,
+      });
+      const valueWithSelection = value.change().select(range).value;
       const result = instance.getTypeahead({
         text: '=ba',
         prefix: 'ba',
         wrapperClasses: ['context-labels'],
-        metric: 'foo',
         labelKey: 'bar',
+        value: valueWithSelection,
       });
       expect(result.context).toBe('context-label-values');
-      expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values' }]);
+      expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
     });
 
-    it('returns label suggestions on aggregation context and metric', () => {
+    it('returns label suggestions on aggregation context and metric w/ selector', () => {
       const instance = shallow(
-        <PromQueryField {...defaultProps} labelKeys={{ foo: ['bar'] }} />
+        <PromQueryField {...defaultProps} labelKeys={{ '{__name__="metric",foo="xx"}': ['bar'] }} />
       ).instance() as PromQueryField;
+      const value = Plain.deserialize('sum(metric{foo="xx"}) by ()');
+      const range = value.selection.merge({
+        anchorOffset: 26,
+      });
+      const valueWithSelection = value.change().select(range).value;
       const result = instance.getTypeahead({
-        text: 'job',
-        prefix: 'job',
+        text: '',
+        prefix: '',
         wrapperClasses: ['context-aggregation'],
-        metric: 'foo',
+        value: valueWithSelection,
+      });
+      expect(result.context).toBe('context-aggregation');
+      expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+    });
+
+    it('returns label suggestions on aggregation context and metric w/o selector', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} labelKeys={{ '{__name__="metric"}': ['bar'] }} />
+      ).instance() as PromQueryField;
+      const value = Plain.deserialize('sum(metric) by ()');
+      const range = value.selection.merge({
+        anchorOffset: 16,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.getTypeahead({
+        text: '',
+        prefix: '',
+        wrapperClasses: ['context-aggregation'],
+        value: valueWithSelection,
       });
       expect(result.context).toBe('context-aggregation');
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
index eb8fc25c67f..c6119cc9d0f 100644
--- a/public/app/containers/Explore/PromQueryField.tsx
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -1,12 +1,13 @@
 import _ from 'lodash';
 import React from 'react';
+import { Value } from 'slate';
 
 // dom also includes Element polyfills
 import { getNextCharacter, getPreviousCousin } from './utils/dom';
 import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
 import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
 import RunnerPlugin from './slate-plugins/runner';
-import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
+import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus';
 
 import TypeaheadField, {
   Suggestion,
@@ -16,7 +17,8 @@ import TypeaheadField, {
   TypeaheadOutput,
 } from './QueryField';
 
-const EMPTY_METRIC = '';
+const DEFAULT_KEYS = ['job', 'instance'];
+const EMPTY_SELECTOR = '{}';
 const METRIC_MARK = 'metric';
 const PRISM_LANGUAGE = 'promql';
 
@@ -77,8 +79,8 @@ interface PromTypeaheadInput {
   text: string;
   prefix: string;
   wrapperClasses: string[];
-  metric?: string;
   labelKey?: string;
+  value?: Value;
 }
 
 class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryFieldState> {
@@ -119,25 +121,23 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
   };
 
   onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
-    const { editorNode, prefix, text, wrapperNode } = typeahead;
+    const { prefix, text, value, wrapperNode } = typeahead;
 
     // Get DOM-dependent context
     const wrapperClasses = Array.from(wrapperNode.classList);
-    // Take first metric as lucky guess
-    const metricNode = editorNode.querySelector(`.${METRIC_MARK}`);
-    const metric = metricNode && metricNode.textContent;
     const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name');
     const labelKey = labelKeyNode && labelKeyNode.textContent;
+    const nextChar = getNextCharacter();
 
-    const result = this.getTypeahead({ text, prefix, wrapperClasses, metric, labelKey });
+    const result = this.getTypeahead({ text, value, prefix, wrapperClasses, labelKey });
 
-    console.log('handleTypeahead', wrapperClasses, text, prefix, result.context);
+    console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
 
     return result;
   };
 
   // Keep this DOM-free for testing
-  getTypeahead({ prefix, wrapperClasses, metric, text }: PromTypeaheadInput): TypeaheadOutput {
+  getTypeahead({ prefix, wrapperClasses, text }: PromTypeaheadInput): TypeaheadOutput {
     // Determine candidates by CSS context
     if (_.includes(wrapperClasses, 'context-range')) {
       // Suggestions for metric[|]
@@ -145,12 +145,11 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     } else if (_.includes(wrapperClasses, 'context-labels')) {
       // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
       return this.getLabelTypeahead.apply(this, arguments);
-    } else if (metric && _.includes(wrapperClasses, 'context-aggregation')) {
+    } else if (_.includes(wrapperClasses, 'context-aggregation')) {
       return this.getAggregationTypeahead.apply(this, arguments);
     } else if (
-      // Non-empty but not inside known token unless it's a metric
+      // Non-empty but not inside known token
       (prefix && !_.includes(wrapperClasses, 'token')) ||
-      prefix === metric ||
       (prefix === '' && !text.match(/^[)\s]+$/)) || // Empty context or after ')'
       text.match(/[+\-*/^%]/) // After binary operator
     ) {
@@ -191,14 +190,27 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     };
   }
 
-  getAggregationTypeahead({ metric }: PromTypeaheadInput): TypeaheadOutput {
+  getAggregationTypeahead({ value }: PromTypeaheadInput): TypeaheadOutput {
     let refresher: Promise<any> = null;
     const suggestions: SuggestionGroup[] = [];
-    const labelKeys = this.state.labelKeys[metric];
+
+    // sum(foo{bar="1"}) by (|)
+    const line = value.anchorBlock.getText();
+    const cursorOffset: number = value.anchorOffset;
+    // sum(foo{bar="1"}) by (
+    const leftSide = line.slice(0, cursorOffset);
+    const openParensAggregationIndex = leftSide.lastIndexOf('(');
+    const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('(');
+    const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
+    // foo{bar="1"}
+    const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
+    const selector = getCleanSelector(selectorString, selectorString.length - 2);
+
+    const labelKeys = this.state.labelKeys[selector];
     if (labelKeys) {
       suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
     } else {
-      refresher = this.fetchMetricLabels(metric);
+      refresher = this.fetchSeriesLabels(selector);
     }
 
     return {
@@ -208,59 +220,51 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     };
   }
 
-  getLabelTypeahead({ metric, text, wrapperClasses, labelKey }: PromTypeaheadInput): TypeaheadOutput {
+  getLabelTypeahead({ text, wrapperClasses, labelKey, value }: PromTypeaheadInput): TypeaheadOutput {
     let context: string;
     let refresher: Promise<any> = null;
     const suggestions: SuggestionGroup[] = [];
-    if (metric) {
-      const labelKeys = this.state.labelKeys[metric];
-      if (labelKeys) {
-        if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
-          // Label values
-          if (labelKey) {
-            const labelValues = this.state.labelValues[metric][labelKey];
-            context = 'context-label-values';
-            suggestions.push({
-              label: 'Label values',
-              items: labelValues.map(wrapLabel),
-            });
-          }
-        } else {
-          // Label keys
-          context = 'context-labels';
-          suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
-        }
-      } else {
-        refresher = this.fetchMetricLabels(metric);
+    const line = value.anchorBlock.getText();
+    const cursorOffset: number = value.anchorOffset;
+
+    // Get normalized selector
+    let selector;
+    try {
+      selector = getCleanSelector(line, cursorOffset);
+    } catch {
+      selector = EMPTY_SELECTOR;
+    }
+    const containsMetric = selector.indexOf('__name__=') > -1;
+
+    if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
+      // Label values
+      if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) {
+        const labelValues = this.state.labelValues[selector][labelKey];
+        context = 'context-label-values';
+        suggestions.push({
+          label: `Label values for "${labelKey}"`,
+          items: labelValues.map(wrapLabel),
+        });
       }
     } else {
-      // Metric-independent label queries
-      const defaultKeys = ['job', 'instance'];
-      // Munge all keys that we have seen together
-      const labelKeys = Object.keys(this.state.labelKeys).reduce((acc, metric) => {
-        return acc.concat(this.state.labelKeys[metric].filter(key => acc.indexOf(key) === -1));
-      }, defaultKeys);
-      if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
-        // Label values
-        if (labelKey) {
-          if (this.state.labelValues[EMPTY_METRIC]) {
-            const labelValues = this.state.labelValues[EMPTY_METRIC][labelKey];
-            context = 'context-label-values';
-            suggestions.push({
-              label: 'Label values',
-              items: labelValues.map(wrapLabel),
-            });
-          } else {
-            // Can only query label values for now (API to query keys is under development)
-            refresher = this.fetchLabelValues(labelKey);
-          }
-        }
-      } else {
-        // Label keys
+      // Label keys
+      const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
+      if (labelKeys) {
         context = 'context-labels';
-        suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
+        suggestions.push({ label: `Labels`, items: labelKeys.map(wrapLabel) });
       }
     }
+
+    // Query labels for selector
+    if (selector && !this.state.labelValues[selector]) {
+      if (selector === EMPTY_SELECTOR) {
+        // Query label values for default labels
+        refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
+      } else {
+        refresher = this.fetchSeriesLabels(selector, !containsMetric);
+      }
+    }
+
     return { context, refresher, suggestions };
   }
 
@@ -276,14 +280,14 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     try {
       const res = await this.request(url);
       const body = await (res.data || res.json());
-      const pairs = this.state.labelValues[EMPTY_METRIC];
+      const exisingValues = this.state.labelValues[EMPTY_SELECTOR];
       const values = {
-        ...pairs,
+        ...exisingValues,
         [key]: body.data,
       };
       const labelValues = {
         ...this.state.labelValues,
-        [EMPTY_METRIC]: values,
+        [EMPTY_SELECTOR]: values,
       };
       this.setState({ labelValues });
     } catch (e) {
@@ -291,12 +295,12 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     }
   }
 
-  async fetchMetricLabels(name) {
+  async fetchSeriesLabels(name, withName?) {
     const url = `/api/v1/series?match[]=${name}`;
     try {
       const res = await this.request(url);
       const body = await (res.data || res.json());
-      const { keys, values } = processLabels(body.data);
+      const { keys, values } = processLabels(body.data, withName);
       const labelKeys = {
         ...this.state.labelKeys,
         [name]: keys,
diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx
index 60caddcad31..238549c1303 100644
--- a/public/app/containers/Explore/QueryField.tsx
+++ b/public/app/containers/Explore/QueryField.tsx
@@ -126,6 +126,7 @@ export interface TypeaheadInput {
   prefix: string;
   selection?: Selection;
   text: string;
+  value: Value;
   wrapperNode: Element;
 }
 
@@ -199,6 +200,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
   handleTypeahead = _.debounce(async () => {
     const selection = window.getSelection();
     const { cleanText, onTypeahead } = this.props;
+    const { value } = this.state;
 
     if (onTypeahead && selection.anchorNode) {
       const wrapperNode = selection.anchorNode.parentElement;
@@ -221,6 +223,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
         prefix,
         selection,
         text,
+        value,
         wrapperNode,
       });
 
diff --git a/public/app/containers/Explore/utils/prometheus.jest.ts b/public/app/containers/Explore/utils/prometheus.jest.ts
new file mode 100644
index 00000000000..febaecc29b5
--- /dev/null
+++ b/public/app/containers/Explore/utils/prometheus.jest.ts
@@ -0,0 +1,33 @@
+import { getCleanSelector } from './prometheus';
+
+describe('getCleanSelector()', () => {
+  it('returns a clean selector from an empty selector', () => {
+    expect(getCleanSelector('{}', 1)).toBe('{}');
+  });
+  it('throws if selector is broken', () => {
+    expect(() => getCleanSelector('{foo')).toThrow();
+  });
+  it('returns the selector sorted by label key', () => {
+    expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}');
+    expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",foo="bar"}');
+  });
+  it('returns a clean selector from an incomplete one', () => {
+    expect(getCleanSelector('{foo}')).toBe('{}');
+    expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}');
+    expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}');
+  });
+  it('throws if not inside a selector', () => {
+    expect(() => getCleanSelector('foo{}', 0)).toThrow();
+    expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow();
+  });
+  it('returns the selector nearest to the cursor offset', () => {
+    expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
+    expect(getCleanSelector('{foo="bar"} + {foo="bar"}', 1)).toBe('{foo="bar"}');
+    expect(getCleanSelector('{foo="bar"} + {baz="xx"}', 1)).toBe('{foo="bar"}');
+    expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}');
+  });
+  it('returns a selector with metric if metric is given', () => {
+    expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}');
+    expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}');
+  });
+});
diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts
index 30f9c25b8f7..ab77271076d 100644
--- a/public/app/containers/Explore/utils/prometheus.ts
+++ b/public/app/containers/Explore/utils/prometheus.ts
@@ -1,9 +1,16 @@
 export const RATE_RANGES = ['1m', '5m', '10m', '30m', '1h'];
 
-export function processLabels(labels) {
+export function processLabels(labels, withName = false) {
   const values = {};
   labels.forEach(l => {
     const { __name__, ...rest } = l;
+    if (withName) {
+      values['__name__'] = values['__name__'] || [];
+      if (values['__name__'].indexOf(__name__) === -1) {
+        values['__name__'].push(__name__);
+      }
+    }
+
     Object.keys(rest).forEach(key => {
       if (!values[key]) {
         values[key] = [];
@@ -18,3 +25,64 @@ export function processLabels(labels) {
 
 // Strip syntax chars
 export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
+
+// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
+const selectorRegexp = /\{[^}]*?\}/;
+const labelRegexp = /\b\w+="[^"\n]*?"/g;
+export function getCleanSelector(query: string, cursorOffset = 1): string {
+  if (!query.match(selectorRegexp)) {
+    // Special matcher for metrics
+    if (query.match(/^\w+$/)) {
+      return `{__name__="${query}"}`;
+    }
+    throw new Error('Query must contain a selector: ' + query);
+  }
+
+  // Check if inside a selector
+  const prefix = query.slice(0, cursorOffset);
+  const prefixOpen = prefix.lastIndexOf('{');
+  const prefixClose = prefix.lastIndexOf('}');
+  if (prefixOpen === -1) {
+    throw new Error('Not inside selector, missing open brace: ' + prefix);
+  }
+  if (prefixClose > -1 && prefixClose > prefixOpen) {
+    throw new Error('Not inside selector, previous selector already closed: ' + prefix);
+  }
+  const suffix = query.slice(cursorOffset);
+  const suffixCloseIndex = suffix.indexOf('}');
+  const suffixClose = suffixCloseIndex + cursorOffset;
+  const suffixOpenIndex = suffix.indexOf('{');
+  const suffixOpen = suffixOpenIndex + cursorOffset;
+  if (suffixClose === -1) {
+    throw new Error('Not inside selector, missing closing brace in suffix: ' + suffix);
+  }
+  if (suffixOpenIndex > -1 && suffixOpen < suffixClose) {
+    throw new Error('Not inside selector, next selector opens before this one closed: ' + suffix);
+  }
+
+  // Extract clean labels to form clean selector, incomplete labels are dropped
+  const selector = query.slice(prefixOpen, suffixClose);
+  let labels = {};
+  selector.replace(labelRegexp, match => {
+    const delimiterIndex = match.indexOf('=');
+    const key = match.slice(0, delimiterIndex);
+    const value = match.slice(delimiterIndex + 1, match.length);
+    labels[key] = value;
+    return '';
+  });
+
+  // Add metric if there is one before the selector
+  const metricPrefix = query.slice(0, prefixOpen);
+  const metricMatch = metricPrefix.match(/\w+$/);
+  if (metricMatch) {
+    labels['__name__'] = `"${metricMatch[0]}"`;
+  }
+
+  // Build sorted selector
+  const cleanSelector = Object.keys(labels)
+    .sort()
+    .map(key => `${key}=${labels[key]}`)
+    .join(',');
+
+  return ['{', cleanSelector, '}'].join('');
+}

From 4e33314c141c0267d0a44e44497bb2c6edca3e99 Mon Sep 17 00:00:00 2001
From: dadosch <daniel-github@dadosch.de>
Date: Mon, 6 Aug 2018 14:40:30 +0200
Subject: [PATCH 225/380] unix socket docs

---
 docs/sources/installation/configuration.md | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 2a799b044b3..d81d8a8dcec 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -181,7 +181,7 @@ embedded database (included in the main Grafana binary).
 
 ### url
 
-Use either URL or or the other fields below to configure the database
+Use either URL or the other fields below to configure the database
 Example: `mysql://user:secret@host:port/database`
 
 ### type
@@ -195,9 +195,9 @@ will be stored.
 
 ### host
 
-Only applicable to MySQL or Postgres. Includes IP or hostname and port.
+Only applicable to MySQL or Postgres. Includes IP or hostname and port or in case of unix sockets the path to it.
 For example, for MySQL running on the same host as Grafana: `host =
-127.0.0.1:3306`
+127.0.0.1:3306` or with unix sockets: `host = /var/run/mysqld/mysqld.sock`
 
 ### name
 
@@ -697,9 +697,9 @@ session provider you have configured.
 
 - **file:** session file path, e.g. `data/sessions`
 - **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
-- **postgres:** ex:  user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full
-- **memcache:** ex:  127.0.0.1:11211
-- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`
+- **postgres:** ex:  `user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full`
+- **memcache:** ex:  `127.0.0.1:11211`
+- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`. For unix socket, use for example: `network=unix,addr=/var/run/redis/redis.sock,pool_size=100,db=grafana`
 
 Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default).
 

From eaff7b0f68844769266f6fd795644b1d058c837a Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Thu, 2 Aug 2018 16:43:33 +0200
Subject: [PATCH 226/380] Explore: Add history to query fields

- queries are saved to localstorage history array
- one history per datasource type (plugin ID)
- 100 items kept with timestamps
- history suggestions can be pulled up with Ctrl-SPACE
---
 public/app/containers/Explore/Explore.tsx     | 50 ++++++++++++++++---
 .../app/containers/Explore/PromQueryField.tsx | 45 ++++++++++++++++-
 public/app/containers/Explore/QueryField.tsx  |  8 ++-
 public/app/containers/Explore/QueryRows.tsx   |  7 +--
 public/app/core/specs/store.jest.ts           | 12 +++++
 public/app/core/store.ts                      | 32 ++++++++++++
 6 files changed, 142 insertions(+), 12 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index a0bb38a13f1..e4de96dbdf2 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -4,6 +4,7 @@ import Select from 'react-select';
 
 import kbn from 'app/core/utils/kbn';
 import colors from 'app/core/utils/colors';
+import store from 'app/core/store';
 import TimeSeries from 'app/core/time_series2';
 import { decodePathComponent } from 'app/core/utils/location_util';
 import { parse as parseDate } from 'app/core/utils/datemath';
@@ -16,6 +17,8 @@ import Table from './Table';
 import TimePicker, { DEFAULT_RANGE } from './TimePicker';
 import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
 
+const MAX_HISTORY_ITEMS = 100;
+
 function makeTimeSeriesList(dataList, options) {
   return dataList.map((seriesData, index) => {
     const datapoints = seriesData.datapoints || [];
@@ -56,6 +59,7 @@ interface IExploreState {
   datasourceLoading: boolean | null;
   datasourceMissing: boolean;
   graphResult: any;
+  history: any[];
   initialDatasource?: string;
   latency: number;
   loading: any;
@@ -86,6 +90,7 @@ export class Explore extends React.Component<any, IExploreState> {
       datasourceMissing: false,
       graphResult: null,
       initialDatasource: datasource,
+      history: [],
       latency: 0,
       loading: false,
       logsResult: null,
@@ -138,6 +143,7 @@ export class Explore extends React.Component<any, IExploreState> {
     const supportsGraph = datasource.meta.metrics;
     const supportsLogs = datasource.meta.logs;
     const supportsTable = datasource.meta.metrics;
+    const datasourceId = datasource.meta.id;
     let datasourceError = null;
 
     try {
@@ -147,10 +153,14 @@ export class Explore extends React.Component<any, IExploreState> {
       datasourceError = (error && error.statusText) || error;
     }
 
+    const historyKey = `grafana.explore.history.${datasourceId}`;
+    const history = store.getObject(historyKey, []);
+
     this.setState(
       {
         datasource,
         datasourceError,
+        history,
         supportsGraph,
         supportsLogs,
         supportsTable,
@@ -269,6 +279,27 @@ export class Explore extends React.Component<any, IExploreState> {
     }
   };
 
+  onQuerySuccess(datasourceId: string, queries: any[]): void {
+    // save queries to history
+    let { datasource, history } = this.state;
+    if (datasource.meta.id !== datasourceId) {
+      // Navigated away, queries did not matter
+      return;
+    }
+    const ts = Date.now();
+    queries.forEach(q => {
+      const { query } = q;
+      history = [...history, { query, ts }];
+    });
+    if (history.length > MAX_HISTORY_ITEMS) {
+      history = history.slice(history.length - MAX_HISTORY_ITEMS);
+    }
+    // Combine all queries of a datasource type into one history
+    const historyKey = `grafana.explore.history.${datasourceId}`;
+    store.setObject(historyKey, history);
+    this.setState({ history });
+  }
+
   buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
     const { datasource, queries, range } = this.state;
     const resolution = this.el.offsetWidth;
@@ -301,6 +332,7 @@ export class Explore extends React.Component<any, IExploreState> {
       const result = makeTimeSeriesList(res.data, options);
       const latency = Date.now() - now;
       this.setState({ latency, loading: false, graphResult: result, requestOptions: options });
+      this.onQuerySuccess(datasource.meta.id, queries);
     } catch (response) {
       console.error(response);
       const queryError = response.data ? response.data.error : response;
@@ -324,6 +356,7 @@ export class Explore extends React.Component<any, IExploreState> {
       const tableModel = res.data[0];
       const latency = Date.now() - now;
       this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
+      this.onQuerySuccess(datasource.meta.id, queries);
     } catch (response) {
       console.error(response);
       const queryError = response.data ? response.data.error : response;
@@ -347,6 +380,7 @@ export class Explore extends React.Component<any, IExploreState> {
       const logsData = res.data;
       const latency = Date.now() - now;
       this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
+      this.onQuerySuccess(datasource.meta.id, queries);
     } catch (response) {
       console.error(response);
       const queryError = response.data ? response.data.error : response;
@@ -367,6 +401,7 @@ export class Explore extends React.Component<any, IExploreState> {
       datasourceLoading,
       datasourceMissing,
       graphResult,
+      history,
       latency,
       loading,
       logsResult,
@@ -405,12 +440,12 @@ export class Explore extends React.Component<any, IExploreState> {
               </a>
             </div>
           ) : (
-              <div className="navbar-buttons explore-first-button">
-                <button className="btn navbar-button" onClick={this.handleClickCloseSplit}>
-                  Close Split
+            <div className="navbar-buttons explore-first-button">
+              <button className="btn navbar-button" onClick={this.handleClickCloseSplit}>
+                Close Split
               </button>
-              </div>
-            )}
+            </div>
+          )}
           {!datasourceMissing ? (
             <div className="navbar-buttons">
               <Select
@@ -470,6 +505,7 @@ export class Explore extends React.Component<any, IExploreState> {
         {datasource && !datasourceError ? (
           <div className="explore-container">
             <QueryRows
+              history={history}
               queries={queries}
               request={this.request}
               onAddQueryRow={this.handleAddQueryRow}
@@ -488,7 +524,9 @@ export class Explore extends React.Component<any, IExploreState> {
                   split={split}
                 />
               ) : null}
-              {supportsTable && showingTable ? <Table data={tableResult} onClickCell={this.onClickTableCell} className="m-t-3" /> : null}
+              {supportsTable && showingTable ? (
+                <Table data={tableResult} onClickCell={this.onClickTableCell} className="m-t-3" />
+              ) : null}
               {supportsLogs && showingLogs ? <Logs data={logsResult} /> : null}
             </main>
           </div>
diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
index c6119cc9d0f..274f604a7fb 100644
--- a/public/app/containers/Explore/PromQueryField.tsx
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -1,4 +1,5 @@
 import _ from 'lodash';
+import moment from 'moment';
 import React from 'react';
 import { Value } from 'slate';
 
@@ -19,6 +20,8 @@ import TypeaheadField, {
 
 const DEFAULT_KEYS = ['job', 'instance'];
 const EMPTY_SELECTOR = '{}';
+const HISTORY_ITEM_COUNT = 5;
+const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
 const METRIC_MARK = 'metric';
 const PRISM_LANGUAGE = 'promql';
 
@@ -28,6 +31,22 @@ export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
   return suggestion;
 };
 
+export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion {
+  const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
+  const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label);
+  const count = historyForItem.length;
+  const recent = historyForItem.pop();
+  let hint = `Queried ${count} times in the last 24h.`;
+  if (recent) {
+    const lastQueried = moment(recent.ts).fromNow();
+    hint = `${hint} Last queried ${lastQueried}.`;
+  }
+  return {
+    ...item,
+    documentation: hint,
+  };
+}
+
 export function willApplySuggestion(
   suggestion: string,
   { typeaheadContext, typeaheadText }: TypeaheadFieldState
@@ -59,6 +78,7 @@ export function willApplySuggestion(
 }
 
 interface PromQueryFieldProps {
+  history?: any[];
   initialQuery?: string | null;
   labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
   labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
@@ -162,17 +182,38 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
   }
 
   getEmptyTypeahead(): TypeaheadOutput {
+    const { history } = this.props;
+    const { metrics } = this.state;
     const suggestions: SuggestionGroup[] = [];
+
+    if (history && history.length > 0) {
+      const historyItems = _.chain(history)
+        .uniqBy('query')
+        .takeRight(HISTORY_ITEM_COUNT)
+        .map(h => h.query)
+        .map(wrapLabel)
+        .map(item => addHistoryMetadata(item, history))
+        .reverse()
+        .value();
+
+      suggestions.push({
+        prefixMatch: true,
+        skipSort: true,
+        label: 'History',
+        items: historyItems,
+      });
+    }
+
     suggestions.push({
       prefixMatch: true,
       label: 'Functions',
       items: FUNCTIONS.map(setFunctionMove),
     });
 
-    if (this.state.metrics) {
+    if (metrics) {
       suggestions.push({
         label: 'Metrics',
-        items: this.state.metrics.map(wrapLabel),
+        items: metrics.map(wrapLabel),
       });
     }
     return { suggestions };
diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx
index 238549c1303..e261eb3ca80 100644
--- a/public/app/containers/Explore/QueryField.tsx
+++ b/public/app/containers/Explore/QueryField.tsx
@@ -97,6 +97,10 @@ export interface SuggestionGroup {
    * If true, do not filter items in this group based on the search.
    */
   skipFilter?: boolean;
+  /**
+   * If true, do not sort items.
+   */
+  skipSort?: boolean;
 }
 
 interface TypeaheadFieldProps {
@@ -244,7 +248,9 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
               group.items = group.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix);
             }
 
-            group.items = _.sortBy(group.items, item => item.sortText || item.label);
+            if (!group.skipSort) {
+              group.items = _.sortBy(group.items, item => item.sortText || item.label);
+            }
           }
           return group;
         })
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx
index d2c1d81607f..bc8972e0660 100644
--- a/public/app/containers/Explore/QueryRows.tsx
+++ b/public/app/containers/Explore/QueryRows.tsx
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
 
 import QueryField from './PromQueryField';
 
-class QueryRow extends PureComponent<any, any> {
+class QueryRow extends PureComponent<any, {}> {
   handleChangeQuery = value => {
     const { index, onChangeQuery } = this.props;
     if (onChangeQuery) {
@@ -32,7 +32,7 @@ class QueryRow extends PureComponent<any, any> {
   };
 
   render() {
-    const { request, query, edited } = this.props;
+    const { edited, history, query, request } = this.props;
     return (
       <div className="query-row">
         <div className="query-row-tools">
@@ -46,6 +46,7 @@ class QueryRow extends PureComponent<any, any> {
         <div className="slate-query-field-wrapper">
           <QueryField
             initialQuery={edited ? null : query}
+            history={history}
             portalPrefix="explore"
             onPressEnter={this.handlePressEnter}
             onQueryChange={this.handleChangeQuery}
@@ -57,7 +58,7 @@ class QueryRow extends PureComponent<any, any> {
   }
 }
 
-export default class QueryRows extends PureComponent<any, any> {
+export default class QueryRows extends PureComponent<any, {}> {
   render() {
     const { className = '', queries, ...handlers } = this.props;
     return (
diff --git a/public/app/core/specs/store.jest.ts b/public/app/core/specs/store.jest.ts
index 0162960621d..ac02501f99e 100644
--- a/public/app/core/specs/store.jest.ts
+++ b/public/app/core/specs/store.jest.ts
@@ -32,6 +32,18 @@ describe('store', () => {
     expect(store.getBool('key5', false)).toBe(true);
   });
 
+  it('gets an object', () => {
+    expect(store.getObject('object1')).toBeUndefined();
+    expect(store.getObject('object1', [])).toEqual([]);
+    store.setObject('object1', [1]);
+    expect(store.getObject('object1')).toEqual([1]);
+  });
+
+  it('sets an object', () => {
+    expect(store.setObject('object2', { a: 1 })).toBe(true);
+    expect(store.getObject('object2')).toEqual({ a: 1 });
+  });
+
   it('key should be deleted', () => {
     store.set('key6', '123');
     store.delete('key6');
diff --git a/public/app/core/store.ts b/public/app/core/store.ts
index b0714f49256..7cc969cf97f 100644
--- a/public/app/core/store.ts
+++ b/public/app/core/store.ts
@@ -14,6 +14,38 @@ export class Store {
     return window.localStorage[key] === 'true';
   }
 
+  getObject(key: string, def?: any) {
+    let ret = def;
+    if (this.exists(key)) {
+      const json = window.localStorage[key];
+      try {
+        ret = JSON.parse(json);
+      } catch (error) {
+        console.error(`Error parsing store object: ${key}. Returning default: ${def}. [${error}]`);
+      }
+    }
+    return ret;
+  }
+
+  // Returns true when successfully stored
+  setObject(key: string, value: any): boolean {
+    let json;
+    try {
+      json = JSON.stringify(value);
+    } catch (error) {
+      console.error(`Could not stringify object: ${key}. [${error}]`);
+      return false;
+    }
+    try {
+      this.set(key, json);
+    } catch (error) {
+      // Likely hitting storage quota
+      console.error(`Could not save item in localStorage: ${key}. [${error}]`);
+      return false;
+    }
+    return true;
+  }
+
   exists(key) {
     return window.localStorage[key] !== void 0;
   }

From cda3b01781887e4356eb9b2d8062b8db7c96046c Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 3 Aug 2018 11:40:44 +0200
Subject: [PATCH 227/380] Reversed history direction for explore

- _.reverse() was modifying state.history
---
 public/app/containers/Explore/Explore.tsx        | 4 ++--
 public/app/containers/Explore/PromQueryField.tsx | 5 ++---
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index e4de96dbdf2..31fd082c94c 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -289,10 +289,10 @@ export class Explore extends React.Component<any, IExploreState> {
     const ts = Date.now();
     queries.forEach(q => {
       const { query } = q;
-      history = [...history, { query, ts }];
+      history = [{ query, ts }, ...history];
     });
     if (history.length > MAX_HISTORY_ITEMS) {
-      history = history.slice(history.length - MAX_HISTORY_ITEMS);
+      history = history.slice(0, MAX_HISTORY_ITEMS);
     }
     // Combine all queries of a datasource type into one history
     const historyKey = `grafana.explore.history.${datasourceId}`;
diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
index 274f604a7fb..a527589e7b2 100644
--- a/public/app/containers/Explore/PromQueryField.tsx
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -35,7 +35,7 @@ export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion
   const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
   const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label);
   const count = historyForItem.length;
-  const recent = historyForItem.pop();
+  const recent = historyForItem[0];
   let hint = `Queried ${count} times in the last 24h.`;
   if (recent) {
     const lastQueried = moment(recent.ts).fromNow();
@@ -189,11 +189,10 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     if (history && history.length > 0) {
       const historyItems = _.chain(history)
         .uniqBy('query')
-        .takeRight(HISTORY_ITEM_COUNT)
+        .take(HISTORY_ITEM_COUNT)
         .map(h => h.query)
         .map(wrapLabel)
         .map(item => addHistoryMetadata(item, history))
-        .reverse()
         .value();
 
       suggestions.push({

From 0d9870d9f1c283be414726e42523c29595e21f2b Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 6 Aug 2018 16:26:59 +0200
Subject: [PATCH 228/380] build: failing to push to docker hub fails the build.

---
 packaging/docker/build-deploy.sh       | 1 +
 packaging/docker/push_to_docker_hub.sh | 1 +
 2 files changed, 2 insertions(+)

diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh
index e20ae2c2a41..ac3226a4a61 100755
--- a/packaging/docker/build-deploy.sh
+++ b/packaging/docker/build-deploy.sh
@@ -1,4 +1,5 @@
 #!/bin/sh
+set -e
 
 _grafana_version=$1
 ./build.sh "$_grafana_version"
diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh
index e779b04d68d..3cf97d580ca 100755
--- a/packaging/docker/push_to_docker_hub.sh
+++ b/packaging/docker/push_to_docker_hub.sh
@@ -1,4 +1,5 @@
 #!/bin/sh
+set -e
 
 _grafana_tag=$1
 

From a73fc4a688acdb3e40107f109f0e4d2e33efa5a9 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 6 Aug 2018 17:34:25 +0200
Subject: [PATCH 229/380] Smaller docker image (#12824)

* build: makes the grafana docker image smaller.

* build: branches and PR:s builds the docker image.
---
 .circleci/config.yml        | 22 ++++++++++++++++++++++
 packaging/docker/Dockerfile | 17 +++++++++++++----
 2 files changed, 35 insertions(+), 4 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index e2deab62c1b..8f2e9b6c1af 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -194,6 +194,18 @@ jobs:
       - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
       - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
 
+  grafana-docker-pr:
+    docker:
+      - image: docker:stable-git
+    steps:
+      - checkout
+      - attach_workspace:
+          at: .
+      - setup_remote_docker
+      - run: docker info
+      - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
+      - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
+
   grafana-docker-release:
       docker:
         - image: docker:stable-git
@@ -387,3 +399,13 @@ workflows:
             filters: *filter-not-release-or-master
         - postgres-integration-test:
             filters: *filter-not-release-or-master
+        - grafana-docker-pr:
+            requires:
+              - build
+              - test-backend
+              - test-frontend
+              - codespell
+              - gometalinter
+              - mysql-integration-test
+              - postgres-integration-test
+            filters: *filter-not-release-or-master
diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile
index aaaf333fc6b..e2109b74909 100644
--- a/packaging/docker/Dockerfile
+++ b/packaging/docker/Dockerfile
@@ -1,6 +1,17 @@
 FROM debian:stretch-slim
 
 ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz"
+
+RUN apt-get update && apt-get install -qq -y tar && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/*
+
+COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
+
+RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
+
+FROM debian:stretch-slim
+
 ARG GF_UID="472"
 ARG GF_GID="472"
 
@@ -12,15 +23,13 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
     GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
     GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
 
-RUN apt-get update && apt-get install -qq -y tar libfontconfig ca-certificates && \
+RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
     apt-get autoremove -y && \
     rm -rf /var/lib/apt/lists/*
 
-COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
+COPY --from=0 /tmp/grafana "$GF_PATHS_HOME"
 
 RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
-    tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C "$GF_PATHS_HOME" && \
-    rm /tmp/grafana.tar.gz && \
     groupadd -r -g $GF_GID grafana && \
     useradd -r -u $GF_UID -g grafana grafana && \
     mkdir -p "$GF_PATHS_PROVISIONING/datasources" \

From e115e600dbafed9baa5d10d8d42ec062eceee9f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 7 Aug 2018 11:35:05 +0200
Subject: [PATCH 230/380] Update ROADMAP.md

---
 ROADMAP.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ROADMAP.md b/ROADMAP.md
index 6f8111fd2d4..002811eded7 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,6 +6,7 @@ But it will give you an idea of our current vision and plan.
 ### Short term (1-2 months)
   - Multi-Stat panel
   - Metrics & Log Explore UI 
+  - Backend plugins
  
 ### Mid term (2-4 months)  
   - React Panels 

From 433b0abf6d37c09f42a724f2fdbff9fa7c32d9a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 7 Aug 2018 11:36:17 +0200
Subject: [PATCH 231/380] Update ROADMAP.md

---
 ROADMAP.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ROADMAP.md b/ROADMAP.md
index 002811eded7..37d4c723a7d 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,12 +6,12 @@ But it will give you an idea of our current vision and plan.
 ### Short term (1-2 months)
   - Multi-Stat panel
   - Metrics & Log Explore UI 
-  - Backend plugins
  
 ### Mid term (2-4 months)  
   - React Panels 
   - Change visualization (panel type) on the fly. 
   - Templating Query Editor UI Plugin hook
+  - Backend plugins
   
 ### Long term (4 - 8 months)
 

From 4a387a96552ffd54493c00afd8e7673e90d228d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 7 Aug 2018 11:43:04 +0200
Subject: [PATCH 232/380] Update ROADMAP.md

---
 ROADMAP.md | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/ROADMAP.md b/ROADMAP.md
index 37d4c723a7d..891bc9f790b 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,9 +1,10 @@
-# Roadmap (2018-06-26)
+# Roadmap (2018-08-07)
 
 This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. 
 But it will give you an idea of our current vision and plan. 
   
 ### Short term (1-2 months)
+  - PRs & Bugs
   - Multi-Stat panel
   - Metrics & Log Explore UI 
  
@@ -14,15 +15,13 @@ But it will give you an idea of our current vision and plan.
   - Backend plugins
   
 ### Long term (4 - 8 months)
-
-- Alerting improvements (silence, per series tracking, etc)
-- Progress on React migration
+ - Alerting improvements (silence, per series tracking, etc)
+ - Progress on React migration
 
 ### In a distant future far far away
-
-- Meta queries 
-- Integrated light weight TSDB
-- Web socket & live data sources
+ - Meta queries 
+ - Integrated light weight TSDB
+ - Web socket & live data sources
 
 ### Outside contributions
 We know this is being worked on right now by contributors (and we hope to merge it when it's ready). 

From 0f94d2f5f1c0aae35566260a4dc5f0711e0466c8 Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Tue, 7 Aug 2018 12:34:12 +0200
Subject: [PATCH 233/380] Fix closing parens completion for prometheus queries
 in Explore (#12810)

- position was determined by SPACE, but Prometheus selectors can
  contain spaces
- added negative lookahead to check if space is outside a selector
- moved braces plugin into PromQueryField since braces are prom specific
---
 public/app/containers/Explore/PromQueryField.tsx         | 2 ++
 public/app/containers/Explore/QueryField.tsx             | 3 +--
 .../app/containers/Explore/slate-plugins/braces.jest.ts  | 9 +++++++++
 public/app/containers/Explore/slate-plugins/braces.ts    | 6 ++++--
 4 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
index a527589e7b2..68f31d8ffd6 100644
--- a/public/app/containers/Explore/PromQueryField.tsx
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -7,6 +7,7 @@ import { Value } from 'slate';
 import { getNextCharacter, getPreviousCousin } from './utils/dom';
 import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
 import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
+import BracesPlugin from './slate-plugins/braces';
 import RunnerPlugin from './slate-plugins/runner';
 import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus';
 
@@ -110,6 +111,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     super(props, context);
 
     this.plugins = [
+      BracesPlugin(),
       RunnerPlugin({ handler: props.onPressEnter }),
       PluginPrism({ definition: PrismPromql, language: PRISM_LANGUAGE }),
     ];
diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx
index e261eb3ca80..04481885a1c 100644
--- a/public/app/containers/Explore/QueryField.tsx
+++ b/public/app/containers/Explore/QueryField.tsx
@@ -5,7 +5,6 @@ import { Block, Change, Document, Text, Value } from 'slate';
 import { Editor } from 'slate-react';
 import Plain from 'slate-plain-serializer';
 
-import BracesPlugin from './slate-plugins/braces';
 import ClearPlugin from './slate-plugins/clear';
 import NewlinePlugin from './slate-plugins/newline';
 
@@ -149,7 +148,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
     super(props, context);
 
     // Base plugins
-    this.plugins = [BracesPlugin(), ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins];
+    this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins];
 
     this.state = {
       suggestions: [],
diff --git a/public/app/containers/Explore/slate-plugins/braces.jest.ts b/public/app/containers/Explore/slate-plugins/braces.jest.ts
index 5c9a90ae034..dda805c07f7 100644
--- a/public/app/containers/Explore/slate-plugins/braces.jest.ts
+++ b/public/app/containers/Explore/slate-plugins/braces.jest.ts
@@ -44,4 +44,13 @@ describe('braces', () => {
     handler(event, change);
     expect(Plain.serialize(change.value)).toEqual('(foo) (bar)() ugh');
   });
+
+  it('adds closing braces outside a selector', () => {
+    const change = Plain.deserialize('sumrate(metric{namespace="dev", cluster="c1"}[2m])').change();
+    let event;
+    change.move(3);
+    event = new window.KeyboardEvent('keydown', { key: '(' });
+    handler(event, change);
+    expect(Plain.serialize(change.value)).toEqual('sum(rate(metric{namespace="dev", cluster="c1"}[2m]))');
+  });
 });
diff --git a/public/app/containers/Explore/slate-plugins/braces.ts b/public/app/containers/Explore/slate-plugins/braces.ts
index b92a224d111..2ea58569ef0 100644
--- a/public/app/containers/Explore/slate-plugins/braces.ts
+++ b/public/app/containers/Explore/slate-plugins/braces.ts
@@ -4,6 +4,8 @@ const BRACES = {
   '(': ')',
 };
 
+const NON_SELECTOR_SPACE_REGEXP = / (?![^}]+})/;
+
 export default function BracesPlugin() {
   return {
     onKeyDown(event, change) {
@@ -28,8 +30,8 @@ export default function BracesPlugin() {
           event.preventDefault();
           const text = value.anchorText.text;
           const offset = value.anchorOffset;
-          const space = text.indexOf(' ', offset);
-          const length = space > 0 ? space : text.length;
+          const delimiterIndex = text.slice(offset).search(NON_SELECTOR_SPACE_REGEXP);
+          const length = delimiterIndex > -1 ? delimiterIndex + offset : text.length;
           const forward = length - offset;
           // Insert matching braces
           change

From f1c1633d154ce643321876419af29672e5d283ca Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Sat, 4 Aug 2018 11:07:48 +0200
Subject: [PATCH 234/380] Explore: show message if queries did not return data

- every result viewer displays a message that it received an empty data
  set
---
 public/app/containers/Explore/Explore.tsx | 19 ++++++++++---------
 public/app/containers/Explore/Graph.tsx   |  9 ++++++++-
 public/app/containers/Explore/Logs.tsx    |  1 +
 public/app/containers/Explore/Table.tsx   | 21 +++++++++++++++++++--
 4 files changed, 38 insertions(+), 12 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 31fd082c94c..53c43782ad6 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -440,12 +440,12 @@ export class Explore extends React.Component<any, IExploreState> {
               </a>
             </div>
           ) : (
-            <div className="navbar-buttons explore-first-button">
-              <button className="btn navbar-button" onClick={this.handleClickCloseSplit}>
-                Close Split
+              <div className="navbar-buttons explore-first-button">
+                <button className="btn navbar-button" onClick={this.handleClickCloseSplit}>
+                  Close Split
               </button>
-            </div>
-          )}
+              </div>
+            )}
           {!datasourceMissing ? (
             <div className="navbar-buttons">
               <Select
@@ -513,21 +513,22 @@ export class Explore extends React.Component<any, IExploreState> {
               onExecuteQuery={this.handleSubmit}
               onRemoveQueryRow={this.handleRemoveQueryRow}
             />
-            {queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
+            {queryError && !loading ? <div className="text-warning m-a-2">{queryError}</div> : null}
             <main className="m-t-2">
               {supportsGraph && showingGraph ? (
                 <Graph
                   data={graphResult}
+                  height={graphHeight}
+                  loading={loading}
                   id={`explore-graph-${position}`}
                   options={requestOptions}
-                  height={graphHeight}
                   split={split}
                 />
               ) : null}
               {supportsTable && showingTable ? (
-                <Table data={tableResult} onClickCell={this.onClickTableCell} className="m-t-3" />
+                <Table className="m-t-3" data={tableResult} loading={loading} onClickCell={this.onClickTableCell} />
               ) : null}
-              {supportsLogs && showingLogs ? <Logs data={logsResult} /> : null}
+              {supportsLogs && showingLogs ? <Logs data={logsResult} loading={loading} /> : null}
             </main>
           </div>
         ) : null}
diff --git a/public/app/containers/Explore/Graph.tsx b/public/app/containers/Explore/Graph.tsx
index a43ddfb2aa5..eeda29b1292 100644
--- a/public/app/containers/Explore/Graph.tsx
+++ b/public/app/containers/Explore/Graph.tsx
@@ -123,7 +123,14 @@ class Graph extends Component<any, any> {
   }
 
   render() {
-    const { data, height } = this.props;
+    const { data, height, loading } = this.props;
+    if (!loading && data && data.length === 0) {
+      return (
+        <div className="panel-container">
+          <div className="muted m-a-1">The queries returned no time series to graph.</div>
+        </div>
+      );
+    }
     return (
       <div className="panel-container">
         <div id={this.props.id} className="explore-graph" style={{ height }} />
diff --git a/public/app/containers/Explore/Logs.tsx b/public/app/containers/Explore/Logs.tsx
index 10d7827a9a3..ae2d5e2daa6 100644
--- a/public/app/containers/Explore/Logs.tsx
+++ b/public/app/containers/Explore/Logs.tsx
@@ -5,6 +5,7 @@ import { LogsModel, LogRow } from 'app/core/logs_model';
 interface LogsProps {
   className?: string;
   data: LogsModel;
+  loading: boolean;
 }
 
 const EXAMPLE_QUERY = '{job="default/prometheus"}';
diff --git a/public/app/containers/Explore/Table.tsx b/public/app/containers/Explore/Table.tsx
index 0856acd5d89..5cf41563704 100644
--- a/public/app/containers/Explore/Table.tsx
+++ b/public/app/containers/Explore/Table.tsx
@@ -6,6 +6,7 @@ const EMPTY_TABLE = new TableModel();
 interface TableProps {
   className?: string;
   data: TableModel;
+  loading: boolean;
   onClickCell?: (columnKey: string, rowValue: string) => void;
 }
 
@@ -38,8 +39,24 @@ function Cell(props: SFCCellProps) {
 
 export default class Table extends PureComponent<TableProps, {}> {
   render() {
-    const { className = '', data, onClickCell } = this.props;
-    const tableModel = data || EMPTY_TABLE;
+    const { className = '', data, loading, onClickCell } = this.props;
+    let tableModel = data || EMPTY_TABLE;
+    if (!loading && data && data.rows.length === 0) {
+      return (
+        <table className={`${className} filter-table`}>
+          <thead>
+            <tr>
+              <th>Table</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td className="muted">The queries returned no data for a table.</td>
+            </tr>
+          </tbody>
+        </table>
+      );
+    }
     return (
       <table className={`${className} filter-table`}>
         <thead>

From 00f04f4ea0d0eab8ab1cc724b5431675e98d8d91 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Sat, 4 Aug 2018 11:47:04 +0200
Subject: [PATCH 235/380] Add clear button to Explore

- Clear All button to clear all queries and results
- moved result viewer buttons below query rows to make it more clear
  that they govern result options
---
 public/app/containers/Explore/Explore.tsx | 50 +++++++++++++++--------
 public/app/containers/Explore/Graph.tsx   |  3 +-
 public/sass/pages/_explore.scss           |  8 ++++
 3 files changed, 43 insertions(+), 18 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 53c43782ad6..772617dd7c1 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -267,6 +267,15 @@ export class Explore extends React.Component<any, IExploreState> {
     }
   };
 
+  onClickClear = () => {
+    this.setState({
+      graphResult: null,
+      logsResult: null,
+      queries: ensureQueries(),
+      tableResult: null,
+    });
+  };
+
   onClickTableCell = (columnKey: string, rowValue: string) => {
     const { datasource, queries } = this.state;
     if (datasource && datasource.modifyQuery) {
@@ -466,24 +475,12 @@ export class Explore extends React.Component<any, IExploreState> {
               </button>
             </div>
           ) : null}
-          <div className="navbar-buttons">
-            {supportsGraph ? (
-              <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
-                Graph
-              </button>
-            ) : null}
-            {supportsTable ? (
-              <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
-                Table
-              </button>
-            ) : null}
-            {supportsLogs ? (
-              <button className={`btn navbar-button ${logsButtonActive}`} onClick={this.handleClickLogsButton}>
-                Logs
-              </button>
-            ) : null}
-          </div>
           <TimePicker range={range} onChangeTime={this.handleChangeTime} />
+          <div className="navbar-buttons">
+            <button className="btn navbar-button navbar-button--no-icon" onClick={this.onClickClear}>
+              Clear All
+            </button>
+          </div>
           <div className="navbar-buttons relative">
             <button className="btn navbar-button--primary" onClick={this.handleSubmit}>
               Run Query <i className="fa fa-level-down run-icon" />
@@ -514,6 +511,25 @@ export class Explore extends React.Component<any, IExploreState> {
               onRemoveQueryRow={this.handleRemoveQueryRow}
             />
             {queryError && !loading ? <div className="text-warning m-a-2">{queryError}</div> : null}
+
+            <div className="result-options">
+              {supportsGraph ? (
+                <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
+                  Graph
+                </button>
+              ) : null}
+              {supportsTable ? (
+                <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
+                  Table
+                </button>
+              ) : null}
+              {supportsLogs ? (
+                <button className={`btn navbar-button ${logsButtonActive}`} onClick={this.handleClickLogsButton}>
+                  Logs
+                </button>
+              ) : null}
+            </div>
+
             <main className="m-t-2">
               {supportsGraph && showingGraph ? (
                 <Graph
diff --git a/public/app/containers/Explore/Graph.tsx b/public/app/containers/Explore/Graph.tsx
index eeda29b1292..171be9da6ba 100644
--- a/public/app/containers/Explore/Graph.tsx
+++ b/public/app/containers/Explore/Graph.tsx
@@ -84,7 +84,9 @@ class Graph extends Component<any, any> {
 
   draw() {
     const { data, options: userOptions } = this.props;
+    const $el = $(`#${this.props.id}`);
     if (!data) {
+      $el.empty();
       return;
     }
     const series = data.map((ts: TimeSeries) => ({
@@ -93,7 +95,6 @@ class Graph extends Component<any, any> {
       data: ts.getFlotPairs('null'),
     }));
 
-    const $el = $(`#${this.props.id}`);
     const ticks = $el.width() / 100;
     let { from, to } = userOptions.range;
     if (!moment.isMoment(from)) {
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index 59b8b62f349..52ddbc03636 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -47,6 +47,14 @@
     background-color: $btn-active-bg;
   }
 
+  .navbar-button--no-icon {
+    line-height: 18px;
+  }
+
+  .result-options {
+    margin-top: 2 * $panel-margin;
+  }
+
   .elapsed-time {
     position: absolute;
     left: 0;

From 307248f713d00b889325b353ec9ba47f1c87f914 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Sat, 4 Aug 2018 11:58:54 +0200
Subject: [PATCH 236/380] Add clear row button

- clears the content of a query row
---
 public/app/containers/Explore/Explore.tsx   | 136 ++++++++++----------
 public/app/containers/Explore/QueryRows.tsx |  26 ++--
 public/sass/pages/_explore.scss             |   2 +-
 3 files changed, 87 insertions(+), 77 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 772617dd7c1..b21a78ed8ab 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -166,7 +166,7 @@ export class Explore extends React.Component<any, IExploreState> {
         supportsTable,
         datasourceLoading: false,
       },
-      () => datasourceError === null && this.handleSubmit()
+      () => datasourceError === null && this.onSubmit()
     );
   }
 
@@ -174,7 +174,7 @@ export class Explore extends React.Component<any, IExploreState> {
     this.el = el;
   };
 
-  handleAddQueryRow = index => {
+  onAddQueryRow = index => {
     const { queries } = this.state;
     const nextQueries = [
       ...queries.slice(0, index + 1),
@@ -184,7 +184,7 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState({ queries: nextQueries });
   };
 
-  handleChangeDatasource = async option => {
+  onChangeDatasource = async option => {
     this.setState({
       datasource: null,
       datasourceError: null,
@@ -197,10 +197,10 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setDatasource(datasource);
   };
 
-  handleChangeQuery = (value, index) => {
+  onChangeQuery = (value: string, index: number, override?: boolean) => {
     const { queries } = this.state;
     const prevQuery = queries[index];
-    const edited = prevQuery.query !== value;
+    const edited = override ? false : prevQuery.query !== value;
     const nextQuery = {
       ...queries[index],
       edited,
@@ -211,60 +211,12 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState({ queries: nextQueries });
   };
 
-  handleChangeTime = nextRange => {
+  onChangeTime = nextRange => {
     const range = {
       from: nextRange.from,
       to: nextRange.to,
     };
-    this.setState({ range }, () => this.handleSubmit());
-  };
-
-  handleClickCloseSplit = () => {
-    const { onChangeSplit } = this.props;
-    if (onChangeSplit) {
-      onChangeSplit(false);
-    }
-  };
-
-  handleClickGraphButton = () => {
-    this.setState(state => ({ showingGraph: !state.showingGraph }));
-  };
-
-  handleClickLogsButton = () => {
-    this.setState(state => ({ showingLogs: !state.showingLogs }));
-  };
-
-  handleClickSplit = () => {
-    const { onChangeSplit } = this.props;
-    if (onChangeSplit) {
-      onChangeSplit(true, this.state);
-    }
-  };
-
-  handleClickTableButton = () => {
-    this.setState(state => ({ showingTable: !state.showingTable }));
-  };
-
-  handleRemoveQueryRow = index => {
-    const { queries } = this.state;
-    if (queries.length <= 1) {
-      return;
-    }
-    const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
-    this.setState({ queries: nextQueries }, () => this.handleSubmit());
-  };
-
-  handleSubmit = () => {
-    const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
-    if (showingTable && supportsTable) {
-      this.runTableQuery();
-    }
-    if (showingGraph && supportsGraph) {
-      this.runGraphQuery();
-    }
-    if (showingLogs && supportsLogs) {
-      this.runLogsQuery();
-    }
+    this.setState({ range }, () => this.onSubmit());
   };
 
   onClickClear = () => {
@@ -276,6 +228,32 @@ export class Explore extends React.Component<any, IExploreState> {
     });
   };
 
+  onClickCloseSplit = () => {
+    const { onChangeSplit } = this.props;
+    if (onChangeSplit) {
+      onChangeSplit(false);
+    }
+  };
+
+  onClickGraphButton = () => {
+    this.setState(state => ({ showingGraph: !state.showingGraph }));
+  };
+
+  onClickLogsButton = () => {
+    this.setState(state => ({ showingLogs: !state.showingLogs }));
+  };
+
+  onClickSplit = () => {
+    const { onChangeSplit } = this.props;
+    if (onChangeSplit) {
+      onChangeSplit(true, this.state);
+    }
+  };
+
+  onClickTableButton = () => {
+    this.setState(state => ({ showingTable: !state.showingTable }));
+  };
+
   onClickTableCell = (columnKey: string, rowValue: string) => {
     const { datasource, queries } = this.state;
     if (datasource && datasource.modifyQuery) {
@@ -284,7 +262,29 @@ export class Explore extends React.Component<any, IExploreState> {
         edited: false,
         query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }),
       }));
-      this.setState({ queries: nextQueries }, () => this.handleSubmit());
+      this.setState({ queries: nextQueries }, () => this.onSubmit());
+    }
+  };
+
+  onRemoveQueryRow = index => {
+    const { queries } = this.state;
+    if (queries.length <= 1) {
+      return;
+    }
+    const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
+    this.setState({ queries: nextQueries }, () => this.onSubmit());
+  };
+
+  onSubmit = () => {
+    const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
+    if (showingTable && supportsTable) {
+      this.runTableQuery();
+    }
+    if (showingGraph && supportsGraph) {
+      this.runGraphQuery();
+    }
+    if (showingLogs && supportsLogs) {
+      this.runLogsQuery();
     }
   };
 
@@ -450,7 +450,7 @@ export class Explore extends React.Component<any, IExploreState> {
             </div>
           ) : (
               <div className="navbar-buttons explore-first-button">
-                <button className="btn navbar-button" onClick={this.handleClickCloseSplit}>
+                <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
                   Close Split
               </button>
               </div>
@@ -460,7 +460,7 @@ export class Explore extends React.Component<any, IExploreState> {
               <Select
                 className="datasource-picker"
                 clearable={false}
-                onChange={this.handleChangeDatasource}
+                onChange={this.onChangeDatasource}
                 options={datasources}
                 placeholder="Loading datasources..."
                 value={selectedDatasource}
@@ -470,19 +470,19 @@ export class Explore extends React.Component<any, IExploreState> {
           <div className="navbar__spacer" />
           {position === 'left' && !split ? (
             <div className="navbar-buttons">
-              <button className="btn navbar-button" onClick={this.handleClickSplit}>
+              <button className="btn navbar-button" onClick={this.onClickSplit}>
                 Split
               </button>
             </div>
           ) : null}
-          <TimePicker range={range} onChangeTime={this.handleChangeTime} />
+          <TimePicker range={range} onChangeTime={this.onChangeTime} />
           <div className="navbar-buttons">
             <button className="btn navbar-button navbar-button--no-icon" onClick={this.onClickClear}>
               Clear All
             </button>
           </div>
           <div className="navbar-buttons relative">
-            <button className="btn navbar-button--primary" onClick={this.handleSubmit}>
+            <button className="btn navbar-button--primary" onClick={this.onSubmit}>
               Run Query <i className="fa fa-level-down run-icon" />
             </button>
             {loading || latency ? <ElapsedTime time={latency} className="text-info" /> : null}
@@ -505,26 +505,26 @@ export class Explore extends React.Component<any, IExploreState> {
               history={history}
               queries={queries}
               request={this.request}
-              onAddQueryRow={this.handleAddQueryRow}
-              onChangeQuery={this.handleChangeQuery}
-              onExecuteQuery={this.handleSubmit}
-              onRemoveQueryRow={this.handleRemoveQueryRow}
+              onAddQueryRow={this.onAddQueryRow}
+              onChangeQuery={this.onChangeQuery}
+              onExecuteQuery={this.onSubmit}
+              onRemoveQueryRow={this.onRemoveQueryRow}
             />
             {queryError && !loading ? <div className="text-warning m-a-2">{queryError}</div> : null}
 
             <div className="result-options">
               {supportsGraph ? (
-                <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
+                <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.onClickGraphButton}>
                   Graph
                 </button>
               ) : null}
               {supportsTable ? (
-                <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
+                <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.onClickTableButton}>
                   Table
                 </button>
               ) : null}
               {supportsLogs ? (
-                <button className={`btn navbar-button ${logsButtonActive}`} onClick={this.handleClickLogsButton}>
+                <button className={`btn navbar-button ${logsButtonActive}`} onClick={this.onClickLogsButton}>
                   Logs
                 </button>
               ) : null}
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx
index bc8972e0660..d076d3fd057 100644
--- a/public/app/containers/Explore/QueryRows.tsx
+++ b/public/app/containers/Explore/QueryRows.tsx
@@ -3,28 +3,35 @@ import React, { PureComponent } from 'react';
 import QueryField from './PromQueryField';
 
 class QueryRow extends PureComponent<any, {}> {
-  handleChangeQuery = value => {
+  onChangeQuery = value => {
     const { index, onChangeQuery } = this.props;
     if (onChangeQuery) {
       onChangeQuery(value, index);
     }
   };
 
-  handleClickAddButton = () => {
+  onClickAddButton = () => {
     const { index, onAddQueryRow } = this.props;
     if (onAddQueryRow) {
       onAddQueryRow(index);
     }
   };
 
-  handleClickRemoveButton = () => {
+  onClickClearButton = () => {
+    const { index, onChangeQuery } = this.props;
+    if (onChangeQuery) {
+      onChangeQuery('', index, true);
+    }
+  };
+
+  onClickRemoveButton = () => {
     const { index, onRemoveQueryRow } = this.props;
     if (onRemoveQueryRow) {
       onRemoveQueryRow(index);
     }
   };
 
-  handlePressEnter = () => {
+  onPressEnter = () => {
     const { onExecuteQuery } = this.props;
     if (onExecuteQuery) {
       onExecuteQuery();
@@ -36,20 +43,23 @@ class QueryRow extends PureComponent<any, {}> {
     return (
       <div className="query-row">
         <div className="query-row-tools">
-          <button className="btn navbar-button navbar-button--tight" onClick={this.handleClickAddButton}>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
             <i className="fa fa-plus" />
           </button>
-          <button className="btn navbar-button navbar-button--tight" onClick={this.handleClickRemoveButton}>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
             <i className="fa fa-minus" />
           </button>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
+            <i className="fa fa-times" />
+          </button>
         </div>
         <div className="slate-query-field-wrapper">
           <QueryField
             initialQuery={edited ? null : query}
             history={history}
             portalPrefix="explore"
-            onPressEnter={this.handlePressEnter}
-            onQueryChange={this.handleChangeQuery}
+            onPressEnter={this.onPressEnter}
+            onQueryChange={this.onChangeQuery}
             request={request}
           />
         </div>
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index 52ddbc03636..b08b0db42c8 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -107,7 +107,7 @@
 }
 
 .query-row-tools {
-  width: 4rem;
+  width: 6rem;
 }
 
 .explore {

From 642374de25c539366d139c4aa2ec686fd60dde20 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Sun, 5 Aug 2018 23:07:05 +0200
Subject: [PATCH 237/380] Explore: Metrics chooser for prometheus

- load all histogrammable metrics on start, based on `{le!=''}` series
  query
- select dropdown shows all those metrics as well as histograms in a
  special group
- select a metric will write fill the metric in the query box
- if a histogram is chosen, it will write a complete
  `histogram_quantile` query
- added new dependency: rc-cascader
---
 package.json                                  |   3 +-
 public/app/containers/Explore/Explore.tsx     |   2 +-
 .../app/containers/Explore/PromQueryField.tsx | 118 +++++++++++--
 public/app/containers/Explore/QueryRows.tsx   |  33 ++--
 public/sass/_grafana.scss                     |   1 +
 public/sass/pages/_explore.scss               |  13 ++
 public/vendor/css/rc-cascader.css             | 158 ++++++++++++++++++
 yarn.lock                                     | 123 +++++++++++++-
 8 files changed, 411 insertions(+), 40 deletions(-)
 create mode 100644 public/vendor/css/rc-cascader.css

diff --git a/package.json b/package.json
index c0581c1de43..200285d7a1e 100644
--- a/package.json
+++ b/package.json
@@ -166,6 +166,7 @@
     "mousetrap-global-bind": "^1.1.0",
     "prismjs": "^1.6.0",
     "prop-types": "^15.6.0",
+    "rc-cascader": "^0.14.0",
     "react": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-grid-layout": "0.16.6",
@@ -187,4 +188,4 @@
   "resolutions": {
     "caniuse-db": "1.0.30000772"
   }
-}
\ No newline at end of file
+}
diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index b21a78ed8ab..3ee5bceae8b 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -208,7 +208,7 @@ export class Explore extends React.Component<any, IExploreState> {
     };
     const nextQueries = [...queries];
     nextQueries[index] = nextQuery;
-    this.setState({ queries: nextQueries });
+    this.setState({ queries: nextQueries }, override ? () => this.onSubmit() : undefined);
   };
 
   onChangeTime = nextRange => {
diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
index 68f31d8ffd6..aab31584fd5 100644
--- a/public/app/containers/Explore/PromQueryField.tsx
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -2,6 +2,7 @@ import _ from 'lodash';
 import moment from 'moment';
 import React from 'react';
 import { Value } from 'slate';
+import Cascader from 'rc-cascader';
 
 // dom also includes Element polyfills
 import { getNextCharacter, getPreviousCousin } from './utils/dom';
@@ -21,12 +22,14 @@ import TypeaheadField, {
 
 const DEFAULT_KEYS = ['job', 'instance'];
 const EMPTY_SELECTOR = '{}';
+const HISTOGRAM_GROUP = '__histograms__';
+const HISTOGRAM_SELECTOR = '{le!=""}'; // Returns all timeseries for histograms
 const HISTORY_ITEM_COUNT = 5;
 const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
 const METRIC_MARK = 'metric';
 const PRISM_LANGUAGE = 'promql';
 
-export const wrapLabel = label => ({ label });
+export const wrapLabel = (label: string) => ({ label });
 export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
   suggestion.move = -1;
   return suggestion;
@@ -48,6 +51,22 @@ export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion
   };
 }
 
+export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] {
+  return _.chain(metrics)
+    .groupBy(metric => metric.split(delimiter)[0])
+    .map((metricsForPrefix: string[], prefix: string): CascaderOption => {
+      const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
+      const children = prefixIsMetric ? [] : metricsForPrefix.sort().map(m => ({ label: m, value: m }));
+      return {
+        children,
+        label: prefix,
+        value: prefix,
+      };
+    })
+    .sortBy('label')
+    .value();
+}
+
 export function willApplySuggestion(
   suggestion: string,
   { typeaheadContext, typeaheadText }: TypeaheadFieldState
@@ -78,22 +97,33 @@ export function willApplySuggestion(
   return suggestion;
 }
 
+interface CascaderOption {
+  label: string;
+  value: string;
+  children?: CascaderOption[];
+  disabled?: boolean;
+}
+
 interface PromQueryFieldProps {
   history?: any[];
+  histogramMetrics?: string[];
   initialQuery?: string | null;
   labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
   labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
   metrics?: string[];
+  metricsByPrefix?: CascaderOption[];
   onPressEnter?: () => void;
-  onQueryChange?: (value: string) => void;
+  onQueryChange?: (value: string, override?: boolean) => void;
   portalPrefix?: string;
   request?: (url: string) => any;
 }
 
 interface PromQueryFieldState {
+  histogramMetrics: string[];
   labelKeys: { [index: string]: string[] }; // metric -> [labelKey,...]
   labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
   metrics: string[];
+  metricsByPrefix: CascaderOption[];
 }
 
 interface PromTypeaheadInput {
@@ -107,7 +137,7 @@ interface PromTypeaheadInput {
 class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryFieldState> {
   plugins: any[];
 
-  constructor(props, context) {
+  constructor(props: PromQueryFieldProps, context) {
     super(props, context);
 
     this.plugins = [
@@ -117,21 +147,45 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     ];
 
     this.state = {
+      histogramMetrics: props.histogramMetrics || [],
       labelKeys: props.labelKeys || {},
       labelValues: props.labelValues || {},
       metrics: props.metrics || [],
+      metricsByPrefix: props.metricsByPrefix || [],
     };
   }
 
   componentDidMount() {
     this.fetchMetricNames();
+    this.fetchHistogramMetrics();
   }
 
-  onChangeQuery = value => {
+  onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
+    let query;
+    if (selectedOptions.length === 1) {
+      if (selectedOptions[0].children.length === 0) {
+        query = selectedOptions[0].value;
+      } else {
+        // Ignore click on group
+        return;
+      }
+    } else {
+      const prefix = selectedOptions[0].value;
+      const metric = selectedOptions[1].value;
+      if (prefix === HISTOGRAM_GROUP) {
+        query = `histogram_quantile(0.95, sum(rate(${metric}[5m])) by (le))`;
+      } else {
+        query = metric;
+      }
+    }
+    this.onChangeQuery(query, true);
+  };
+
+  onChangeQuery = (value: string, override?: boolean) => {
     // Send text change to parent
     const { onQueryChange } = this.props;
     if (onQueryChange) {
-      onQueryChange(value);
+      onQueryChange(value, override);
     }
   };
 
@@ -317,7 +371,17 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     return fetch(url);
   };
 
-  async fetchLabelValues(key) {
+  fetchHistogramMetrics() {
+    this.fetchSeriesLabels(HISTOGRAM_SELECTOR, true, () => {
+      const histogramSeries = this.state.labelValues[HISTOGRAM_SELECTOR];
+      if (histogramSeries && histogramSeries['__name__']) {
+        const histogramMetrics = histogramSeries['__name__'].slice().sort();
+        this.setState({ histogramMetrics });
+      }
+    });
+  }
+
+  async fetchLabelValues(key: string) {
     const url = `/api/v1/label/${key}/values`;
     try {
       const res = await this.request(url);
@@ -337,7 +401,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     }
   }
 
-  async fetchSeriesLabels(name, withName?) {
+  async fetchSeriesLabels(name: string, withName?: boolean, callback?: () => void) {
     const url = `/api/v1/series?match[]=${name}`;
     try {
       const res = await this.request(url);
@@ -351,7 +415,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
         ...this.state.labelValues,
         [name]: values,
       };
-      this.setState({ labelKeys, labelValues });
+      this.setState({ labelKeys, labelValues }, callback);
     } catch (e) {
       console.error(e);
     }
@@ -362,23 +426,41 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     try {
       const res = await this.request(url);
       const body = await (res.data || res.json());
-      this.setState({ metrics: body.data }, this.onReceiveMetrics);
+      const metrics = body.data;
+      const metricsByPrefix = groupMetricsByPrefix(metrics);
+      this.setState({ metrics, metricsByPrefix }, this.onReceiveMetrics);
     } catch (error) {
       console.error(error);
     }
   }
 
   render() {
+    const { histogramMetrics, metricsByPrefix } = this.state;
+    const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
+    const metricsOptions = [
+      { label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions },
+      ...metricsByPrefix,
+    ];
+
     return (
-      <TypeaheadField
-        additionalPlugins={this.plugins}
-        cleanText={cleanText}
-        initialValue={this.props.initialQuery}
-        onTypeahead={this.onTypeahead}
-        onWillApplySuggestion={willApplySuggestion}
-        onValueChanged={this.onChangeQuery}
-        placeholder="Enter a PromQL query"
-      />
+      <div className="prom-query-field">
+        <div className="prom-query-field-tools">
+          <Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
+            <button className="btn navbar-button navbar-button--tight">Metrics</button>
+          </Cascader>
+        </div>
+        <div className="slate-query-field-wrapper">
+          <TypeaheadField
+            additionalPlugins={this.plugins}
+            cleanText={cleanText}
+            initialValue={this.props.initialQuery}
+            onTypeahead={this.onTypeahead}
+            onWillApplySuggestion={willApplySuggestion}
+            onValueChanged={this.onChangeQuery}
+            placeholder="Enter a PromQL query"
+          />
+        </div>
+      </div>
     );
   }
 }
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx
index d076d3fd057..cd3cf23a927 100644
--- a/public/app/containers/Explore/QueryRows.tsx
+++ b/public/app/containers/Explore/QueryRows.tsx
@@ -3,10 +3,10 @@ import React, { PureComponent } from 'react';
 import QueryField from './PromQueryField';
 
 class QueryRow extends PureComponent<any, {}> {
-  onChangeQuery = value => {
+  onChangeQuery = (value, override?: boolean) => {
     const { index, onChangeQuery } = this.props;
     if (onChangeQuery) {
-      onChangeQuery(value, index);
+      onChangeQuery(value, index, override);
     }
   };
 
@@ -18,10 +18,7 @@ class QueryRow extends PureComponent<any, {}> {
   };
 
   onClickClearButton = () => {
-    const { index, onChangeQuery } = this.props;
-    if (onChangeQuery) {
-      onChangeQuery('', index, true);
-    }
+    this.onChangeQuery('', true);
   };
 
   onClickRemoveButton = () => {
@@ -42,18 +39,7 @@ class QueryRow extends PureComponent<any, {}> {
     const { edited, history, query, request } = this.props;
     return (
       <div className="query-row">
-        <div className="query-row-tools">
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
-            <i className="fa fa-plus" />
-          </button>
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
-            <i className="fa fa-minus" />
-          </button>
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
-            <i className="fa fa-times" />
-          </button>
-        </div>
-        <div className="slate-query-field-wrapper">
+        <div className="query-row-field">
           <QueryField
             initialQuery={edited ? null : query}
             history={history}
@@ -63,6 +49,17 @@ class QueryRow extends PureComponent<any, {}> {
             request={request}
           />
         </div>
+        <div className="query-row-tools">
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
+            <i className="fa fa-times" />
+          </button>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
+            <i className="fa fa-plus" />
+          </button>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
+            <i className="fa fa-minus" />
+          </button>
+        </div>
       </div>
     );
   }
diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss
index 3a72bd45a1a..ec7103cba95 100644
--- a/public/sass/_grafana.scss
+++ b/public/sass/_grafana.scss
@@ -1,6 +1,7 @@
 // vendor
 @import '../vendor/css/timepicker.css';
 @import '../vendor/css/spectrum.css';
+@import '../vendor/css/rc-cascader.css';
 
 // MIXINS
 @import 'mixins/mixins';
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index b08b0db42c8..6d7ce5a6b61 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -110,6 +110,11 @@
   width: 6rem;
 }
 
+.query-row-field {
+  margin-right: 3px;
+  width: 100%;
+}
+
 .explore {
   .logs {
     .logs-entries {
@@ -146,3 +151,11 @@
     }
   }
 }
+
+// Prometheus-specifics, to be extracted to datasource soon
+
+.explore {
+  .prom-query-field {
+    display: flex;
+  }
+}
diff --git a/public/vendor/css/rc-cascader.css b/public/vendor/css/rc-cascader.css
new file mode 100644
index 00000000000..968c1fc770f
--- /dev/null
+++ b/public/vendor/css/rc-cascader.css
@@ -0,0 +1,158 @@
+.rc-cascader {
+  font-size: 12px;
+}
+.rc-cascader-menus {
+  font-size: 12px;
+  overflow: hidden;
+  background: #fff;
+  position: absolute;
+  border: 1px solid #d9d9d9;
+  border-radius: 6px;
+  box-shadow: 0 0 4px rgba(0, 0, 0, 0.17);
+  white-space: nowrap;
+}
+.rc-cascader-menus-hidden {
+  display: none;
+}
+.rc-cascader-menus.slide-up-enter,
+.rc-cascader-menus.slide-up-appear {
+  animation-duration: .3s;
+  animation-fill-mode: both;
+  transform-origin: 0 0;
+  opacity: 0;
+  animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
+  animation-play-state: paused;
+}
+.rc-cascader-menus.slide-up-leave {
+  animation-duration: .3s;
+  animation-fill-mode: both;
+  transform-origin: 0 0;
+  opacity: 1;
+  animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
+  animation-play-state: paused;
+}
+.rc-cascader-menus.slide-up-enter.slide-up-enter-active.rc-cascader-menus-placement-bottomLeft,
+.rc-cascader-menus.slide-up-appear.slide-up-appear-active.rc-cascader-menus-placement-bottomLeft {
+  animation-name: SlideUpIn;
+  animation-play-state: running;
+}
+.rc-cascader-menus.slide-up-enter.slide-up-enter-active.rc-cascader-menus-placement-topLeft,
+.rc-cascader-menus.slide-up-appear.slide-up-appear-active.rc-cascader-menus-placement-topLeft {
+  animation-name: SlideDownIn;
+  animation-play-state: running;
+}
+.rc-cascader-menus.slide-up-leave.slide-up-leave-active.rc-cascader-menus-placement-bottomLeft {
+  animation-name: SlideUpOut;
+  animation-play-state: running;
+}
+.rc-cascader-menus.slide-up-leave.slide-up-leave-active.rc-cascader-menus-placement-topLeft {
+  animation-name: SlideDownOut;
+  animation-play-state: running;
+}
+.rc-cascader-menu {
+  display: inline-block;
+  /* width: 100px; */
+  max-width: 50vw;
+  height: 192px;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  border-right: 1px solid #e9e9e9;
+  overflow: auto;
+}
+.rc-cascader-menu:last-child {
+  border-right: 0;
+}
+.rc-cascader-menu-item {
+  height: 32px;
+  line-height: 32px;
+  padding: 0 16px;
+  cursor: pointer;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  transition: all 0.3s ease;
+  position: relative;
+}
+.rc-cascader-menu-item:hover {
+  background: #eaf8fe;
+}
+.rc-cascader-menu-item-disabled {
+  cursor: not-allowed;
+  color: #ccc;
+}
+.rc-cascader-menu-item-disabled:hover {
+  background: transparent;
+}
+.rc-cascader-menu-item-loading:after {
+  position: absolute;
+  right: 12px;
+  content: 'loading';
+  color: #aaa;
+  font-style: italic;
+}
+.rc-cascader-menu-item-active {
+  background: #d5f1fd;
+}
+.rc-cascader-menu-item-active:hover {
+  background: #d5f1fd;
+}
+.rc-cascader-menu-item-expand {
+  position: relative;
+}
+.rc-cascader-menu-item-expand:after {
+  content: '>';
+  font-size: 12px;
+  color: #999;
+  position: absolute;
+  right: 16px;
+  line-height: 32px;
+}
+@keyframes SlideUpIn {
+  0% {
+    opacity: 0;
+    transform-origin: 0% 0%;
+    transform: scaleY(0.8);
+  }
+  100% {
+    opacity: 1;
+    transform-origin: 0% 0%;
+    transform: scaleY(1);
+  }
+}
+@keyframes SlideUpOut {
+  0% {
+    opacity: 1;
+    transform-origin: 0% 0%;
+    transform: scaleY(1);
+  }
+  100% {
+    opacity: 0;
+    transform-origin: 0% 0%;
+    transform: scaleY(0.8);
+  }
+}
+@keyframes SlideDownIn {
+  0% {
+    opacity: 0;
+    transform-origin: 0% 100%;
+    transform: scaleY(0.8);
+  }
+  100% {
+    opacity: 1;
+    transform-origin: 0% 100%;
+    transform: scaleY(1);
+  }
+}
+@keyframes SlideDownOut {
+  0% {
+    opacity: 1;
+    transform-origin: 0% 100%;
+    transform: scaleY(1);
+  }
+  100% {
+    opacity: 0;
+    transform-origin: 0% 100%;
+    transform: scaleY(0.8);
+  }
+}
diff --git a/yarn.lock b/yarn.lock
index 6e737e33348..ed8a1eabec3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -478,6 +478,12 @@ acorn@~2.6.4:
   version "2.6.4"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.6.4.tgz#eb1f45b4a43fa31d03701a5ec46f3b52673e90ee"
 
+add-dom-event-listener@1.x:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.0.2.tgz#8faed2c41008721cf111da1d30d995b85be42bed"
+  dependencies:
+    object-assign "4.x"
+
 after@0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -771,6 +777,10 @@ array-slice@^0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
 
+array-tree-filter@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-1.0.1.tgz#0a8ad1eefd38ce88858632f9cc0423d7634e4d5d"
+
 array-union@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@@ -1514,7 +1524,7 @@ babel-register@^6.26.0, babel-register@^6.9.0:
     mkdirp "^0.5.1"
     source-map-support "^0.4.15"
 
-babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2:
+babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   dependencies:
@@ -2246,6 +2256,10 @@ classnames@2.x, classnames@^2.2.4, classnames@^2.2.5:
   version "2.2.5"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
 
+classnames@^2.2.6:
+  version "2.2.6"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
+
 clean-css@3.4.x, clean-css@~3.4.2:
   version "3.4.28"
   resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
@@ -2553,6 +2567,12 @@ component-bind@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
 
+component-classes@^1.2.5:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691"
+  dependencies:
+    component-indexof "0.0.3"
+
 component-emitter@1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3"
@@ -2561,6 +2581,10 @@ component-emitter@1.2.1, component-emitter@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
 
+component-indexof@0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24"
+
 component-inherit@0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
@@ -2841,6 +2865,13 @@ crypto-random-string@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
 
+css-animation@^1.3.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.4.1.tgz#5b8813125de0fbbbb0bbe1b472ae84221469b7a8"
+  dependencies:
+    babel-runtime "6.x"
+    component-classes "^1.2.5"
+
 css-color-names@0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@@ -3515,6 +3546,10 @@ doctrine@^1.2.2:
     esutils "^2.0.2"
     isarray "^1.0.0"
 
+dom-align@^1.7.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.0.tgz#c0e89b5b674c6e836cd248c52c2992135f093654"
+
 dom-converter@~0.1:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
@@ -7354,6 +7389,10 @@ lodash._createset@~4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
 
+lodash._getnative@^3.0.0:
+  version "3.9.1"
+  resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+
 lodash._root@~3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
@@ -7386,6 +7425,14 @@ lodash.flattendeep@^4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
 
+lodash.isarguments@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
+
+lodash.isarray@^3.0.0:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+
 lodash.isequal@^4.0.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
@@ -7406,6 +7453,14 @@ lodash.kebabcase@^4.0.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
 
+lodash.keys@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
+  dependencies:
+    lodash._getnative "^3.0.0"
+    lodash.isarguments "^3.0.0"
+    lodash.isarray "^3.0.0"
+
 lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -8651,7 +8706,7 @@ object-assign@4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
 
-object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
 
@@ -9981,6 +10036,54 @@ raw-body@2.3.3:
     iconv-lite "0.4.23"
     unpipe "1.0.0"
 
+rc-align@^2.4.0:
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.3.tgz#b9b3c2a6d68adae71a8e1d041cd5e3b2a655f99a"
+  dependencies:
+    babel-runtime "^6.26.0"
+    dom-align "^1.7.0"
+    prop-types "^15.5.8"
+    rc-util "^4.0.4"
+
+rc-animate@2.x:
+  version "2.4.4"
+  resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.4.4.tgz#a05a784c747beef140d99ff52b6117711bef4b1e"
+  dependencies:
+    babel-runtime "6.x"
+    css-animation "^1.3.2"
+    prop-types "15.x"
+
+rc-cascader@^0.14.0:
+  version "0.14.0"
+  resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-0.14.0.tgz#a956c99896f10883bf63d46fb894d0cb326842a4"
+  dependencies:
+    array-tree-filter "^1.0.0"
+    prop-types "^15.5.8"
+    rc-trigger "^2.2.0"
+    rc-util "^4.0.4"
+    shallow-equal "^1.0.0"
+    warning "^4.0.1"
+
+rc-trigger@^2.2.0:
+  version "2.5.4"
+  resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.5.4.tgz#9088a24ba5a811b254f742f004e38a9e2f8843fb"
+  dependencies:
+    babel-runtime "6.x"
+    classnames "^2.2.6"
+    prop-types "15.x"
+    rc-align "^2.4.0"
+    rc-animate "2.x"
+    rc-util "^4.4.0"
+
+rc-util@^4.0.4, rc-util@^4.4.0:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.5.1.tgz#0e435057174c024901c7600ba8903dd03da3ab39"
+  dependencies:
+    add-dom-event-listener "1.x"
+    babel-runtime "6.x"
+    prop-types "^15.5.10"
+    shallowequal "^0.2.2"
+
 rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -10980,6 +11083,16 @@ shallow-clone@^1.0.0:
     kind-of "^5.0.0"
     mixin-object "^2.0.1"
 
+shallow-equal@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.0.0.tgz#508d1838b3de590ab8757b011b25e430900945f7"
+
+shallowequal@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e"
+  dependencies:
+    lodash.keys "^3.1.2"
+
 shallowequal@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.0.2.tgz#1561dbdefb8c01408100319085764da3fcf83f8f"
@@ -12555,6 +12668,12 @@ walker@~1.0.5:
   dependencies:
     makeerror "1.0.x"
 
+warning@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.1.tgz#66ce376b7fbfe8a887c22bdf0e7349d73d397745"
+  dependencies:
+    loose-envify "^1.0.0"
+
 watch@~0.18.0:
   version "0.18.0"
   resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"

From a0da66610ec92a24fe6f0735393fde722ba08d9d Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 7 Aug 2018 14:48:22 +0200
Subject: [PATCH 238/380] Fix url param errors

---
 public/app/core/services/keybindingSrv.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index 0930a16d797..e5e7006dcd7 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -21,7 +21,8 @@ export class KeybindingSrv {
     private datasourceSrv,
     private timeSrv,
     private contextSrv,
-    private $window
+    private $window,
+    private $route
   ) {
     // clear out all shortcuts on route change
     $rootScope.$on('$routeChangeSuccess', () => {
@@ -271,7 +272,8 @@ export class KeybindingSrv {
     this.bind('d a', () => {
       this.$location.search('autofitpanels', this.$location.search().autofitpanels ? null : true);
       //Force reload
-      this.$window.location.href = this.$location.absUrl();
+
+      this.$route.reload();
     });
   }
 }

From 2ad358215af9f2655c987d241c1ff1384f264f81 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 7 Aug 2018 14:49:11 +0200
Subject: [PATCH 239/380] Remove window

---
 public/app/core/services/keybindingSrv.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index e5e7006dcd7..f740718063c 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -21,7 +21,6 @@ export class KeybindingSrv {
     private datasourceSrv,
     private timeSrv,
     private contextSrv,
-    private $window,
     private $route
   ) {
     // clear out all shortcuts on route change

From e9746db5ab125b9c6c67203f5726b6429467757c Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 7 Aug 2018 17:02:46 +0200
Subject: [PATCH 240/380] changelog: update #12768

[skip ci]
---
 CHANGELOG.md | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5298dcd04f6..b9d0670c717 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,12 @@
 
 * Postgres datasource no longer automatically adds time column alias when using the $__timeGroup alias. However, there's code in place which should make this change backward compatible and shouldn't create any issues.
 
+### New experimental features
+
+These are new features that's still being worked on and are in an experimental phase. We incourage users to try these out and provide any feedback in related issue.
+
+* **Dashboard**: Auto fit dashboard panels to optimize space used for current TV / Monitor [#12768](https://github.com/grafana/grafana/issues/12768)
+
 # 5.2.2 (2018-07-25)
 
 ### Minor

From 2961c3b3b912db6b9ba9e4b088216bd149dad74d Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Tue, 7 Aug 2018 17:42:00 +0200
Subject: [PATCH 241/380] Review feedback

- use color variables for cascader styles
- fix Table value type
---
 public/app/containers/Explore/Table.tsx       |  9 ++++++-
 public/sass/_grafana.scss                     |  2 +-
 .../css/{rc-cascader.css => rc-cascader.scss} | 24 ++++++++++---------
 3 files changed, 22 insertions(+), 13 deletions(-)
 rename public/vendor/css/{rc-cascader.css => rc-cascader.scss} (88%)

diff --git a/public/app/containers/Explore/Table.tsx b/public/app/containers/Explore/Table.tsx
index 5cf41563704..e5adde2d008 100644
--- a/public/app/containers/Explore/Table.tsx
+++ b/public/app/containers/Explore/Table.tsx
@@ -66,7 +66,14 @@ export default class Table extends PureComponent<TableProps, {}> {
           {tableModel.rows.map((row, i) => (
             <tr key={i}>
               {row.map((value, j) => (
-                <Cell key={j} columnIndex={j} rowIndex={i} value={value} table={data} onClickCell={onClickCell} />
+                <Cell
+                  key={j}
+                  columnIndex={j}
+                  rowIndex={i}
+                  value={String(value)}
+                  table={data}
+                  onClickCell={onClickCell}
+                />
               ))}
             </tr>
           ))}
diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss
index ec7103cba95..be3a3b90f78 100644
--- a/public/sass/_grafana.scss
+++ b/public/sass/_grafana.scss
@@ -1,7 +1,7 @@
 // vendor
 @import '../vendor/css/timepicker.css';
 @import '../vendor/css/spectrum.css';
-@import '../vendor/css/rc-cascader.css';
+@import '../vendor/css/rc-cascader.scss';
 
 // MIXINS
 @import 'mixins/mixins';
diff --git a/public/vendor/css/rc-cascader.css b/public/vendor/css/rc-cascader.scss
similarity index 88%
rename from public/vendor/css/rc-cascader.css
rename to public/vendor/css/rc-cascader.scss
index 968c1fc770f..5cfaaf4961a 100644
--- a/public/vendor/css/rc-cascader.css
+++ b/public/vendor/css/rc-cascader.scss
@@ -4,11 +4,11 @@
 .rc-cascader-menus {
   font-size: 12px;
   overflow: hidden;
-  background: #fff;
+  background: $panel-bg;
   position: absolute;
-  border: 1px solid #d9d9d9;
-  border-radius: 6px;
-  box-shadow: 0 0 4px rgba(0, 0, 0, 0.17);
+  border: $panel-border;
+  border-radius: $border-radius;
+  box-shadow: $typeahead-shadow;
   white-space: nowrap;
 }
 .rc-cascader-menus-hidden {
@@ -57,7 +57,7 @@
   list-style: none;
   margin: 0;
   padding: 0;
-  border-right: 1px solid #e9e9e9;
+  border-right: $panel-border;
   overflow: auto;
 }
 .rc-cascader-menu:last-child {
@@ -75,11 +75,11 @@
   position: relative;
 }
 .rc-cascader-menu-item:hover {
-  background: #eaf8fe;
+  background: $typeahead-selected-bg;
 }
 .rc-cascader-menu-item-disabled {
   cursor: not-allowed;
-  color: #ccc;
+  color: $text-color-weak;
 }
 .rc-cascader-menu-item-disabled:hover {
   background: transparent;
@@ -88,14 +88,16 @@
   position: absolute;
   right: 12px;
   content: 'loading';
-  color: #aaa;
+  color: $text-color-weak;
   font-style: italic;
 }
 .rc-cascader-menu-item-active {
-  background: #d5f1fd;
+  color: $typeahead-selected-color;
+  background: $typeahead-selected-bg;
 }
 .rc-cascader-menu-item-active:hover {
-  background: #d5f1fd;
+  color: $typeahead-selected-color;
+  background: $typeahead-selected-bg;
 }
 .rc-cascader-menu-item-expand {
   position: relative;
@@ -103,7 +105,7 @@
 .rc-cascader-menu-item-expand:after {
   content: '>';
   font-size: 12px;
-  color: #999;
+  color: $text-color-weak;
   position: absolute;
   right: 16px;
   line-height: 32px;

From eb1b9405b2f8b410ff28479abe4192de365b9a79 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 7 Aug 2018 17:56:02 +0200
Subject: [PATCH 242/380] return proper payload from api when updating
 datasource

---
 pkg/api/datasources.go | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go
index 6ffefea991a..23dbb221d71 100644
--- a/pkg/api/datasources.go
+++ b/pkg/api/datasources.go
@@ -158,12 +158,26 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response {
 		}
 		return Error(500, "Failed to update datasource", err)
 	}
-	ds := convertModelToDtos(cmd.Result)
+
+	query := m.GetDataSourceByIdQuery{
+		Id:    cmd.Id,
+		OrgId: c.OrgId,
+	}
+
+	if err := bus.Dispatch(&query); err != nil {
+		if err == m.ErrDataSourceNotFound {
+			return Error(404, "Data source not found", nil)
+		}
+		return Error(500, "Failed to query datasources", err)
+	}
+
+	dtos := convertModelToDtos(query.Result)
+
 	return JSON(200, util.DynMap{
 		"message":    "Datasource updated",
 		"id":         cmd.Id,
 		"name":       cmd.Name,
-		"datasource": ds,
+		"datasource": dtos,
 	})
 }
 

From ee7602ec1fd8e1303dc12a3c7f6fc105228e2893 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 7 Aug 2018 21:01:41 +0200
Subject: [PATCH 243/380] change fillmode from last to previous

---
 docs/sources/features/datasources/mssql.md    |  2 +-
 docs/sources/features/datasources/mysql.md    |  2 +-
 docs/sources/features/datasources/postgres.md |  3 +--
 pkg/tsdb/mssql/macros.go                      |  4 ++--
 pkg/tsdb/mssql/macros_test.go                 |  6 +++---
 pkg/tsdb/mysql/macros.go                      |  4 ++--
 pkg/tsdb/mysql/mysql_test.go                  |  4 ++--
 pkg/tsdb/postgres/macros.go                   |  4 ++--
 pkg/tsdb/postgres/postgres_test.go            |  4 ++--
 pkg/tsdb/sql_engine.go                        | 10 +++++-----
 10 files changed, 21 insertions(+), 22 deletions(-)

diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
index 9a149df120d..caaf5a6b321 100644
--- a/docs/sources/features/datasources/mssql.md
+++ b/docs/sources/features/datasources/mssql.md
@@ -83,7 +83,7 @@ Macro example | Description
 *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value. <br/>For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*.
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
 *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
-*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index 4f4efb6e29a..cdb78deed35 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -66,7 +66,7 @@ Macro example | Description
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
 *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
-*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index f2b54d3f0ce..2be2db0837b 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -63,8 +63,7 @@ Macro example | Description
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300*
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
 *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
-*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used.
-*$__timeGroup(dateColumn,'5m', last)* | Same as above but the last seen value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
+*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+).
 *$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+).
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index 57a37d618e0..42e47ce6d3c 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -102,8 +102,8 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			switch args[2] {
 			case "NULL":
 				m.query.Model.Set("fillMode", "null")
-			case "last":
-				m.query.Model.Set("fillMode", "last")
+			case "previous":
+				m.query.Model.Set("fillMode", "previous")
 			default:
 				m.query.Model.Set("fillMode", "value")
 				floatVal, err := strconv.ParseFloat(args[2], 64)
diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go
index b808666d967..8362ae05aa6 100644
--- a/pkg/tsdb/mssql/macros_test.go
+++ b/pkg/tsdb/mssql/macros_test.go
@@ -85,8 +85,8 @@ func TestMacroEngine(t *testing.T) {
 				So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
 			})
 
-			Convey("interpolate __timeGroup function with fill (value = last)", func() {
-				_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', last)")
+			Convey("interpolate __timeGroup function with fill (value = previous)", func() {
+				_, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', previous)")
 
 				fill := query.Model.Get("fill").MustBool()
 				fillMode := query.Model.Get("fillMode").MustString()
@@ -94,7 +94,7 @@ func TestMacroEngine(t *testing.T) {
 
 				So(err, ShouldBeNil)
 				So(fill, ShouldBeTrue)
-				So(fillMode, ShouldEqual, "last")
+				So(fillMode, ShouldEqual, "previous")
 				So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
 			})
 
diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index bebf4b396bb..905d424f29a 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -97,8 +97,8 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			switch args[2] {
 			case "NULL":
 				m.query.Model.Set("fillMode", "null")
-			case "last":
-				m.query.Model.Set("fillMode", "last")
+			case "previous":
+				m.query.Model.Set("fillMode", "previous")
 			default:
 				m.query.Model.Set("fillMode", "value")
 				floatVal, err := strconv.ParseFloat(args[2], 64)
diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
index fe262a3f758..ca6df8e360e 100644
--- a/pkg/tsdb/mysql/mysql_test.go
+++ b/pkg/tsdb/mysql/mysql_test.go
@@ -321,12 +321,12 @@ func TestMySQL(t *testing.T) {
 				So(points[3][0].Float64, ShouldEqual, 1.5)
 			})
 
-			Convey("When doing a metric query using timeGroup with last fill enabled", func() {
+			Convey("When doing a metric query using timeGroup with previous fill enabled", func() {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
 							Model: simplejson.NewFromAny(map[string]interface{}{
-								"rawSql": "SELECT $__timeGroup(time, '5m', last) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"rawSql": "SELECT $__timeGroup(time, '5m', previous) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
 							}),
 							RefId: "A",
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 3ab21ea0c6e..aebdc55d1d7 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -119,8 +119,8 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			switch args[2] {
 			case "NULL":
 				m.query.Model.Set("fillMode", "null")
-			case "last":
-				m.query.Model.Set("fillMode", "last")
+			case "previous":
+				m.query.Model.Set("fillMode", "previous")
 			default:
 				m.query.Model.Set("fillMode", "value")
 				floatVal, err := strconv.ParseFloat(args[2], 64)
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index ac0964e912c..9e363529df1 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -303,12 +303,12 @@ func TestPostgres(t *testing.T) {
 			})
 		})
 
-		Convey("When doing a metric query using timeGroup with last fill enabled", func() {
+		Convey("When doing a metric query using timeGroup with previous fill enabled", func() {
 			query := &tsdb.TsdbQuery{
 				Queries: []*tsdb.Query{
 					{
 						Model: simplejson.NewFromAny(map[string]interface{}{
-							"rawSql": "SELECT $__timeGroup(time, '5m', last), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+							"rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 							"format": "time_series",
 						}),
 						RefId: "A",
diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index f2f8b17db5f..cbf6d6b4d60 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -274,14 +274,14 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 	fillMissing := query.Model.Get("fill").MustBool(false)
 	var fillInterval float64
 	fillValue := null.Float{}
-	fillLast := false
+	fillPrevious := false
 
 	if fillMissing {
 		fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
 		switch query.Model.Get("fillMode").MustString() {
 		case "null":
-		case "last":
-			fillLast = true
+		case "previous":
+			fillPrevious = true
 		case "value":
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
@@ -358,7 +358,7 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
 				}
 
-				if fillLast {
+				if fillPrevious {
 					if len(series.Points) > 0 {
 						fillValue = series.Points[len(series.Points)-1][0]
 					} else {
@@ -391,7 +391,7 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows,
 			intervalStart := series.Points[len(series.Points)-1][1].Float64
 			intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6)
 
-			if fillLast {
+			if fillPrevious {
 				if len(series.Points) > 0 {
 					fillValue = series.Points[len(series.Points)-1][0]
 				} else {

From 52c7edf2f41e4c3479b39e401b4e1778c461f581 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 7 Aug 2018 21:11:51 +0200
Subject: [PATCH 244/380] rename last fillmode to previous

---
 public/app/plugins/datasource/mssql/partials/query.editor.html  | 2 +-
 public/app/plugins/datasource/mysql/partials/query.editor.html  | 2 +-
 .../app/plugins/datasource/postgres/partials/query.editor.html  | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html
index e873d60ebbf..7888e36a24c 100644
--- a/public/app/plugins/datasource/mssql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mssql/partials/query.editor.html
@@ -55,7 +55,7 @@ Macros:
 - $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
 - $__timeGroup(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300.
      by setting fillvalue grafana will fill in missing values according to the interval
-     fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet
+     fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time]
 
 Example of group by and order by with $__timeGroup:
diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html
index 664481ec8dc..7c799eec21b 100644
--- a/public/app/plugins/datasource/mysql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mysql/partials/query.editor.html
@@ -55,7 +55,7 @@ Macros:
 - $__unixEpochFilter(column) -&gt;  time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
 - $__timeGroup(column,'5m'[, fillvalue]) -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
      by setting fillvalue grafana will fill in missing values according to the interval
-     fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet
+     fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time"
 
 Example of group by and order by with $__timeGroup:
diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html
index c455c0ebaf9..20353b81ba2 100644
--- a/public/app/plugins/datasource/postgres/partials/query.editor.html
+++ b/public/app/plugins/datasource/postgres/partials/query.editor.html
@@ -55,7 +55,7 @@ Macros:
 - $__unixEpochFilter(column) -&gt;  column &gt;= 1492750877 AND column &lt;= 1492750877
 - $__timeGroup(column,'5m'[, fillvalue]) -&gt; (extract(epoch from column)/300)::bigint*300
      by setting fillvalue grafana will fill in missing values according to the interval
-     fillvalue can be either a literal value, NULL or last; last will fill in the last seen value or NULL if none has been seen yet
+     fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300 AS "time"
 
 Example of group by and order by with $__timeGroup:

From a156b6ee06a4b0610430afb254c23242154f1452 Mon Sep 17 00:00:00 2001
From: Ben de Luca <bdeluca@gmail.com>
Date: Tue, 7 Aug 2018 22:32:02 +0200
Subject: [PATCH 245/380] fix missing *

The missing * causes the text to be in the box to be displayed incorrectly.
---
 docs/sources/features/datasources/elasticsearch.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md
index 31ce78f0bfe..d29327cf480 100644
--- a/docs/sources/features/datasources/elasticsearch.md
+++ b/docs/sources/features/datasources/elasticsearch.md
@@ -115,7 +115,7 @@ The Elasticsearch data source supports two types of queries you can use in the *
 
 Query | Description
 ------------ | -------------
-*{"find": "fields", "type": "keyword"} | Returns a list of field names with the index type `keyword`.
+*{"find": "fields", "type": "keyword"}* | Returns a list of field names with the index type `keyword`.
 *{"find": "terms", "field": "@hostname", "size": 1000}* |  Returns a list of values for a field using term aggregation. Query will user current dashboard time range as time range for query.
 *{"find": "terms", "field": "@hostname", "query": '<lucene query>'}* | Returns a list of values for a field using term aggregation & and a specified lucene query filter. Query will use current dashboard time range as time range for query.
 

From e8dfbe94b1e1d6832dfb3acd11dae8b01a8fa6d3 Mon Sep 17 00:00:00 2001
From: tariq1890 <tariq181290@gmail.com>
Date: Sun, 5 Aug 2018 13:54:06 -0700
Subject: [PATCH 246/380] Fixing bug in url query reader and added test cases

---
 pkg/util/url.go             |  2 +-
 pkg/util/url_test.go        | 27 +++++++++++++++++++++++++++
 pkg/util/validation_test.go | 22 ++++++++++++++++++++++
 3 files changed, 50 insertions(+), 1 deletion(-)
 create mode 100644 pkg/util/validation_test.go

diff --git a/pkg/util/url.go b/pkg/util/url.go
index c82dcef67c5..fad2d79a6d0 100644
--- a/pkg/util/url.go
+++ b/pkg/util/url.go
@@ -10,7 +10,7 @@ type UrlQueryReader struct {
 }
 
 func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) {
-	u, err := url.ParseQuery(urlInfo.String())
+	u, err := url.ParseQuery(urlInfo.RawQuery)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/util/url_test.go b/pkg/util/url_test.go
index 4dd221b9e0b..ee29956f60d 100644
--- a/pkg/util/url_test.go
+++ b/pkg/util/url_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	. "github.com/smartystreets/goconvey/convey"
+	"net/url"
 )
 
 func TestUrl(t *testing.T) {
@@ -43,4 +44,30 @@ func TestUrl(t *testing.T) {
 
 		So(result, ShouldEqual, "http://localhost:8080/api/")
 	})
+
+	Convey("When joining two urls where lefthand side has a trailing slash and righthand side has preceding slash", t, func() {
+		result := JoinUrlFragments("http://localhost:8080/", "/api/")
+
+		So(result, ShouldEqual, "http://localhost:8080/api/")
+	})
+}
+
+func TestNewUrlQueryReader(t *testing.T) {
+	u, _ := url.Parse("http://www.abc.com/foo?bar=baz&bar2=baz2")
+	uqr, _ := NewUrlQueryReader(u)
+
+	Convey("when trying to retrieve the first query value", t, func() {
+		result := uqr.Get("bar", "foodef")
+		So(result, ShouldEqual, "baz")
+	})
+
+	Convey("when trying to retrieve the second query value", t, func() {
+		result := uqr.Get("bar2", "foodef")
+		So(result, ShouldEqual, "baz2")
+	})
+
+	Convey("when trying to retrieve from a non-existent key, the default value is returned", t, func() {
+		result := uqr.Get("bar3", "foodef")
+		So(result, ShouldEqual, "foodef")
+	})
 }
diff --git a/pkg/util/validation_test.go b/pkg/util/validation_test.go
new file mode 100644
index 00000000000..124da1b744b
--- /dev/null
+++ b/pkg/util/validation_test.go
@@ -0,0 +1,22 @@
+package util
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestIsEmail(t *testing.T) {
+
+	Convey("When validating a string that is a valid email", t, func() {
+		result := IsEmail("abc@def.com")
+
+		So(result, ShouldEqual, true)
+	})
+
+	Convey("When validating a string that is not a valid email", t, func() {
+		result := IsEmail("abcdef.com")
+
+		So(result, ShouldEqual, false)
+	})
+}

From a6a29f0b2071619ee9a64029542cc27a6b125367 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 8 Aug 2018 09:13:44 +0200
Subject: [PATCH 247/380] changelog: add notes about closing #11270

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9d0670c717..4fa417be5f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
 * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
 * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
+* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
 
 ### Breaking changes
 

From b0ddc15e1ab7f28c6924e3f8448eea2561fcdb45 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Wed, 8 Aug 2018 09:23:36 +0200
Subject: [PATCH 248/380] team list for profile page + mock teams

---
 public/app/features/org/partials/profile.html | 4 ++--
 public/app/features/org/profile_ctrl.ts       | 7 +++----
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html
index 96540911290..5cbb21f488a 100644
--- a/public/app/features/org/partials/profile.html
+++ b/public/app/features/org/partials/profile.html
@@ -32,13 +32,13 @@
       <thead>
         <tr>
           <th>Name</th>
-          <th>Email</th>
+          <th>Members</th>
         </tr>
       </thead>
       <tbody>
         <tr ng-repeat="team in ctrl.user.teams">
           <td>{{team.name}}</td>
-          <td>{{team.email}}</td>
+          <td>{{team.members}}</td>
         </tr>
       </tbody>
     </table>
diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts
index 1ac950699be..361dfa9e52f 100644
--- a/public/app/features/org/profile_ctrl.ts
+++ b/public/app/features/org/profile_ctrl.ts
@@ -28,12 +28,11 @@ export class ProfileCtrl {
   }
 
   getUserTeams() {
-    console.log(this.backendSrv.get('/api/teams'));
     this.backendSrv.get('/api/user').then(teams => {
       this.user.teams = [
-        { name: 'Backend', email: 'backend@grafana.com', members: 2 },
-        { name: 'Frontend', email: 'frontend@grafana.com', members: 2 },
-        { name: 'Ops', email: 'ops@grafana.com', members: 2 },
+        { name: 'Backend', email: 'backend@grafana.com', members: 5 },
+        { name: 'Frontend', email: 'frontend@grafana.com', members: 4 },
+        { name: 'Ops', email: 'ops@grafana.com', members: 6 },
       ];
       this.showTeamsList = this.user.teams.length > 1;
     });

From 9938835dde3be364b549e4ace3eea1c044256f2d Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 8 Aug 2018 09:47:45 +0200
Subject: [PATCH 249/380] devenv: update sql dashboards

---
 .../datasource_tests_mssql_unittest.json      | 244 +++++++++++++++---
 .../datasource_tests_mysql_unittest.json      | 240 ++++++++++++++---
 .../datasource_tests_postgres_unittest.json   | 243 ++++++++++++++---
 3 files changed, 612 insertions(+), 115 deletions(-)

diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
index 80d3e1a5889..0d291f01a09 100644
--- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1532949769359,
+  "iteration": 1533713720618,
   "links": [],
   "panels": [
     {
@@ -338,8 +338,8 @@
       "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
+        "h": 6,
+        "w": 6,
         "x": 0,
         "y": 7
       },
@@ -421,9 +421,9 @@
       "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 8,
+        "h": 6,
+        "w": 6,
+        "x": 6,
         "y": 7
       },
       "id": 9,
@@ -504,9 +504,9 @@
       "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 16,
+        "h": 6,
+        "w": 6,
+        "x": 12,
         "y": 7
       },
       "id": 10,
@@ -579,6 +579,89 @@
         "alignLevel": null
       }
     },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mssql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 18,
+        "y": 7
+      },
+      "id": 36,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null as zero",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m with fill(previous) and null as zero",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
     {
       "aliasColors": {},
       "bars": true,
@@ -587,10 +670,10 @@
       "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
+        "h": 6,
+        "w": 6,
         "x": 0,
-        "y": 16
+        "y": 13
       },
       "id": 16,
       "legend": {
@@ -670,10 +753,10 @@
       "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 8,
-        "y": 16
+        "h": 6,
+        "w": 6,
+        "x": 6,
+        "y": 13
       },
       "id": 12,
       "legend": {
@@ -753,10 +836,10 @@
       "datasource": "gdev-mssql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 16,
-        "y": 16
+        "h": 6,
+        "w": 6,
+        "x": 12,
+        "y": 13
       },
       "id": 13,
       "legend": {
@@ -828,6 +911,89 @@
         "alignLevel": null
       }
     },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mssql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 18,
+        "y": 13
+      },
+      "id": 37,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize with fill(previous)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
     {
       "aliasColors": {},
       "bars": false,
@@ -839,7 +1005,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 25
+        "y": 19
       },
       "id": 27,
       "legend": {
@@ -926,7 +1092,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 25
+        "y": 19
       },
       "id": 5,
       "legend": {
@@ -1029,7 +1195,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 33
+        "y": 27
       },
       "id": 4,
       "legend": {
@@ -1116,7 +1282,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 33
+        "y": 27
       },
       "id": 28,
       "legend": {
@@ -1201,7 +1367,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 41
+        "y": 35
       },
       "id": 19,
       "legend": {
@@ -1288,7 +1454,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 41
+        "y": 35
       },
       "id": 18,
       "legend": {
@@ -1373,7 +1539,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 49
+        "y": 43
       },
       "id": 17,
       "legend": {
@@ -1460,7 +1626,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 49
+        "y": 43
       },
       "id": 20,
       "legend": {
@@ -1545,7 +1711,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 57
+        "y": 51
       },
       "id": 29,
       "legend": {
@@ -1632,7 +1798,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 57
+        "y": 51
       },
       "id": 30,
       "legend": {
@@ -1719,7 +1885,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 65
+        "y": 59
       },
       "id": 14,
       "legend": {
@@ -1807,7 +1973,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 65
+        "y": 59
       },
       "id": 15,
       "legend": {
@@ -1894,7 +2060,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 73
+        "y": 67
       },
       "id": 25,
       "legend": {
@@ -1982,7 +2148,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 73
+        "y": 67
       },
       "id": 22,
       "legend": {
@@ -2069,7 +2235,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 81
+        "y": 75
       },
       "id": 21,
       "legend": {
@@ -2157,7 +2323,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 81
+        "y": 75
       },
       "id": 26,
       "legend": {
@@ -2244,7 +2410,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 89
+        "y": 83
       },
       "id": 23,
       "legend": {
@@ -2332,7 +2498,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 89
+        "y": 83
       },
       "id": 24,
       "legend": {
@@ -2542,5 +2708,5 @@
   "timezone": "",
   "title": "Datasource tests - MSSQL (unit test)",
   "uid": "GlAqcPgmz",
-  "version": 3
+  "version": 10
 }
\ No newline at end of file
diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
index f684186084a..cec8ebe9d02 100644
--- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1532949531280,
+  "iteration": 1533714324007,
   "links": [],
   "panels": [
     {
@@ -338,8 +338,8 @@
       "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
+        "h": 6,
+        "w": 6,
         "x": 0,
         "y": 7
       },
@@ -421,9 +421,9 @@
       "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 8,
+        "h": 6,
+        "w": 6,
+        "x": 6,
         "y": 7
       },
       "id": 9,
@@ -504,9 +504,9 @@
       "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 16,
+        "h": 6,
+        "w": 6,
+        "x": 12,
         "y": 7
       },
       "id": 10,
@@ -579,6 +579,89 @@
         "alignLevel": null
       }
     },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mysql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 18,
+        "y": 7
+      },
+      "id": 36,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m with fill(previous)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
     {
       "aliasColors": {},
       "bars": true,
@@ -587,10 +670,10 @@
       "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
+        "h": 6,
+        "w": 6,
         "x": 0,
-        "y": 16
+        "y": 13
       },
       "id": 16,
       "legend": {
@@ -670,10 +753,10 @@
       "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 8,
-        "y": 16
+        "h": 6,
+        "w": 6,
+        "x": 6,
+        "y": 13
       },
       "id": 12,
       "legend": {
@@ -753,10 +836,10 @@
       "datasource": "gdev-mysql-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 16,
-        "y": 16
+        "h": 6,
+        "w": 6,
+        "x": 12,
+        "y": 13
       },
       "id": 13,
       "legend": {
@@ -828,6 +911,89 @@
         "alignLevel": null
       }
     },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mysql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 18,
+        "y": 13
+      },
+      "id": 37,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize with fill(previous)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
     {
       "aliasColors": {},
       "bars": false,
@@ -839,7 +1005,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 25
+        "y": 19
       },
       "id": 27,
       "legend": {
@@ -926,7 +1092,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 25
+        "y": 19
       },
       "id": 5,
       "legend": {
@@ -1023,7 +1189,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 33
+        "y": 27
       },
       "id": 4,
       "legend": {
@@ -1110,7 +1276,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 33
+        "y": 27
       },
       "id": 28,
       "legend": {
@@ -1195,7 +1361,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 41
+        "y": 35
       },
       "id": 19,
       "legend": {
@@ -1282,7 +1448,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 41
+        "y": 35
       },
       "id": 18,
       "legend": {
@@ -1367,7 +1533,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 49
+        "y": 43
       },
       "id": 17,
       "legend": {
@@ -1454,7 +1620,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 49
+        "y": 43
       },
       "id": 20,
       "legend": {
@@ -1539,7 +1705,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 57
+        "y": 51
       },
       "id": 14,
       "legend": {
@@ -1627,7 +1793,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 57
+        "y": 51
       },
       "id": 15,
       "legend": {
@@ -1714,7 +1880,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 65
+        "y": 59
       },
       "id": 25,
       "legend": {
@@ -1802,7 +1968,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 65
+        "y": 59
       },
       "id": 22,
       "legend": {
@@ -1889,7 +2055,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 73
+        "y": 67
       },
       "id": 21,
       "legend": {
@@ -1977,7 +2143,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 73
+        "y": 67
       },
       "id": 26,
       "legend": {
@@ -2064,7 +2230,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 81
+        "y": 75
       },
       "id": 23,
       "legend": {
@@ -2152,7 +2318,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 81
+        "y": 75
       },
       "id": 24,
       "legend": {
@@ -2360,5 +2526,5 @@
   "timezone": "",
   "title": "Datasource tests - MySQL (unittest)",
   "uid": "Hmf8FDkmz",
-  "version": 1
+  "version": 9
 }
\ No newline at end of file
diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
index 3c2b34df78c..cc93308e116 100644
--- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1532951521836,
+  "iteration": 1533714184500,
   "links": [],
   "panels": [
     {
@@ -338,8 +338,8 @@
       "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
+        "h": 6,
+        "w": 6,
         "x": 0,
         "y": 7
       },
@@ -421,9 +421,9 @@
       "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 8,
+        "h": 6,
+        "w": 6,
+        "x": 6,
         "y": 7
       },
       "id": 9,
@@ -504,9 +504,9 @@
       "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 16,
+        "h": 6,
+        "w": 6,
+        "x": 12,
         "y": 7
       },
       "id": 10,
@@ -579,6 +579,89 @@
         "alignLevel": null
       }
     },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-postgres-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 18,
+        "y": 7
+      },
+      "id": 36,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m with fill(previous)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
     {
       "aliasColors": {},
       "bars": true,
@@ -587,10 +670,10 @@
       "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
+        "h": 6,
+        "w": 6,
         "x": 0,
-        "y": 16
+        "y": 13
       },
       "id": 16,
       "legend": {
@@ -670,10 +753,10 @@
       "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 8,
-        "y": 16
+        "h": 6,
+        "w": 6,
+        "x": 6,
+        "y": 13
       },
       "id": 12,
       "legend": {
@@ -753,10 +836,10 @@
       "datasource": "gdev-postgres-ds-tests",
       "fill": 2,
       "gridPos": {
-        "h": 9,
-        "w": 8,
-        "x": 16,
-        "y": 16
+        "h": 6,
+        "w": 6,
+        "x": 12,
+        "y": 13
       },
       "id": 13,
       "legend": {
@@ -828,6 +911,89 @@
         "alignLevel": null
       }
     },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-postgres-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 18,
+        "y": 13
+      },
+      "id": 37,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize with fill(previous)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
     {
       "aliasColors": {},
       "bars": false,
@@ -839,7 +1005,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 25
+        "y": 19
       },
       "id": 27,
       "legend": {
@@ -926,7 +1092,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 25
+        "y": 19
       },
       "id": 5,
       "legend": {
@@ -1011,7 +1177,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 33
+        "y": 27
       },
       "id": 4,
       "legend": {
@@ -1098,7 +1264,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 33
+        "y": 27
       },
       "id": 28,
       "legend": {
@@ -1183,7 +1349,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 41
+        "y": 35
       },
       "id": 19,
       "legend": {
@@ -1270,7 +1436,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 41
+        "y": 35
       },
       "id": 18,
       "legend": {
@@ -1355,7 +1521,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 49
+        "y": 43
       },
       "id": 17,
       "legend": {
@@ -1442,7 +1608,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 49
+        "y": 43
       },
       "id": 20,
       "legend": {
@@ -1527,7 +1693,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 57
+        "y": 51
       },
       "id": 14,
       "legend": {
@@ -1615,7 +1781,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 57
+        "y": 51
       },
       "id": 15,
       "legend": {
@@ -1702,7 +1868,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 65
+        "y": 59
       },
       "id": 25,
       "legend": {
@@ -1790,7 +1956,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 65
+        "y": 59
       },
       "id": 22,
       "legend": {
@@ -1877,7 +2043,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 73
+        "y": 67
       },
       "id": 21,
       "legend": {
@@ -1965,7 +2131,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 73
+        "y": 67
       },
       "id": 26,
       "legend": {
@@ -2052,7 +2218,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 81
+        "y": 75
       },
       "id": 23,
       "legend": {
@@ -2140,7 +2306,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 81
+        "y": 75
       },
       "id": 24,
       "legend": {
@@ -2352,6 +2518,5 @@
   "timezone": "",
   "title": "Datasource tests - Postgres (unittest)",
   "uid": "vHQdlVziz",
-  "version": 1
-}
-
+  "version": 9
+}
\ No newline at end of file

From beddfdd86b33a965ba30df121c76ce720e83a809 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 8 Aug 2018 10:26:05 +0200
Subject: [PATCH 250/380] add api route for retrieving teams of signed in user

---
 docs/sources/http_api/user.md | 33 +++++++++++++++++++++++++++++++++
 pkg/api/api.go                |  1 +
 pkg/api/user.go               | 15 +++++++++++++++
 3 files changed, 49 insertions(+)

diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md
index 134c1842851..b9047187b2d 100644
--- a/docs/sources/http_api/user.md
+++ b/docs/sources/http_api/user.md
@@ -363,6 +363,39 @@ Content-Type: application/json
 ]
 ```
 
+## Teams that the actual User is member of
+
+`GET /api/user/teams`
+
+Return a list of all teams that the current user is member of.
+
+**Example Request**:
+
+```http
+GET /api/user/teams HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+  {
+    "id": 1,
+    "orgId": 1,
+    "name": "MyTestTeam",
+    "email": "",
+    "avatarUrl": "\/avatar\/3f49c15916554246daa714b9bd0ee398",
+    "memberCount": 1
+  }
+]
+```
+
 ## Star a dashboard
 
 `POST /api/user/stars/dashboard/:dashboardId`
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 84425fdae3d..906481bbb8a 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -120,6 +120,7 @@ func (hs *HTTPServer) registerRoutes() {
 			userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
 			userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
 			userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
+			userRoute.Get("/teams", Wrap(GetSignedInUserTeamList))
 
 			userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
 			userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))
diff --git a/pkg/api/user.go b/pkg/api/user.go
index 725c623575f..4b916202e65 100644
--- a/pkg/api/user.go
+++ b/pkg/api/user.go
@@ -111,6 +111,21 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
 	return getUserOrgList(c.UserId)
 }
 
+// GET /api/user/teams
+func GetSignedInUserTeamList(c *m.ReqContext) Response {
+	query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return Error(500, "Failed to get user teams", err)
+	}
+
+	for _, team := range query.Result {
+		team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
+	}
+
+	return JSON(200, query.Result)
+}
+
 // GET /api/user/:id/orgs
 func GetUserOrgList(c *m.ReqContext) Response {
 	return getUserOrgList(c.ParamsInt64(":id"))

From 817179c09733fb4d94ab44fea1d28e7152dafadc Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 8 Aug 2018 10:33:30 +0200
Subject: [PATCH 251/380] changelog: add notes about closing #12756

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fa417be5f6..4983dbafdcd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
 * **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
+* **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm)

From ca06893e691b07f938788af65e8d8847e05be9fc Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Wed, 8 Aug 2018 10:50:27 +0200
Subject: [PATCH 252/380] removed mock-teams, now gets teams from backend

---
 public/app/features/org/partials/profile.html |  4 +---
 public/app/features/org/profile_ctrl.ts       | 10 +++-------
 2 files changed, 4 insertions(+), 10 deletions(-)

diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html
index 5cbb21f488a..790872d9789 100644
--- a/public/app/features/org/partials/profile.html
+++ b/public/app/features/org/partials/profile.html
@@ -32,13 +32,11 @@
       <thead>
         <tr>
           <th>Name</th>
-          <th>Members</th>
         </tr>
       </thead>
       <tbody>
-        <tr ng-repeat="team in ctrl.user.teams">
+        <tr ng-repeat="team in ctrl.teams">
           <td>{{team.name}}</td>
-          <td>{{team.members}}</td>
         </tr>
       </tbody>
     </table>
diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts
index 361dfa9e52f..6cfcdc2e64c 100644
--- a/public/app/features/org/profile_ctrl.ts
+++ b/public/app/features/org/profile_ctrl.ts
@@ -28,13 +28,9 @@ export class ProfileCtrl {
   }
 
   getUserTeams() {
-    this.backendSrv.get('/api/user').then(teams => {
-      this.user.teams = [
-        { name: 'Backend', email: 'backend@grafana.com', members: 5 },
-        { name: 'Frontend', email: 'frontend@grafana.com', members: 4 },
-        { name: 'Ops', email: 'ops@grafana.com', members: 6 },
-      ];
-      this.showTeamsList = this.user.teams.length > 1;
+    this.backendSrv.get('/api/user/teams').then(teams => {
+      this.teams = teams;
+      this.showTeamsList = this.teams.length > 1;
     });
   }
 

From a94406ac53f58e4617d30f7cd18d11613ed2476c Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Wed, 8 Aug 2018 11:22:47 +0200
Subject: [PATCH 253/380] added more info about the teams

---
 public/app/features/org/partials/profile.html | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html
index 790872d9789..b204c223138 100644
--- a/public/app/features/org/partials/profile.html
+++ b/public/app/features/org/partials/profile.html
@@ -31,12 +31,18 @@
     <table class="filter-table form-inline">
       <thead>
         <tr>
+          <th></th>
           <th>Name</th>
+          <th>Email</th>
+          <th>Members</th>
         </tr>
       </thead>
       <tbody>
         <tr ng-repeat="team in ctrl.teams">
+          <td class="width-4 text-center"><img class="filter-table__avatar" src={{team.avatarUrl}}></td>
           <td>{{team.name}}</td>
+          <td>{{team.email}}</td>
+          <td>{{team.memberCount}}</td>
         </tr>
       </tbody>
     </table>

From 8dfe4a97efb0389f8c0ea77f823670a01e8361ae Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 8 Aug 2018 16:01:01 +0200
Subject: [PATCH 254/380] use uid when linking to dashboards internally in a
 dashboard

---
 public/app/features/dashlinks/module.ts    | 3 +--
 public/app/features/panellinks/link_srv.ts | 4 ++++
 public/app/features/panellinks/module.ts   | 7 ++++++-
 3 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/public/app/features/dashlinks/module.ts b/public/app/features/dashlinks/module.ts
index 380144dbcd5..6322e39f290 100644
--- a/public/app/features/dashlinks/module.ts
+++ b/public/app/features/dashlinks/module.ts
@@ -144,8 +144,7 @@ export class DashLinksContainerCtrl {
             if (dash.id !== currentDashId) {
               memo.push({
                 title: dash.title,
-                url: 'dashboard/' + dash.uri,
-                target: link.target,
+                url: dash.url,
                 icon: 'fa fa-th-large',
                 keepTime: link.keepTime,
                 includeVars: link.includeVars,
diff --git a/public/app/features/panellinks/link_srv.ts b/public/app/features/panellinks/link_srv.ts
index b20294485a5..9aee17f83ed 100644
--- a/public/app/features/panellinks/link_srv.ts
+++ b/public/app/features/panellinks/link_srv.ts
@@ -77,6 +77,10 @@ export class LinkSrv {
       info.target = link.targetBlank ? '_blank' : '_self';
       info.href = this.templateSrv.replace(link.url || '', scopedVars);
       info.title = this.templateSrv.replace(link.title || '', scopedVars);
+    } else if (link.url) {
+      info.href = link.url;
+      info.title = this.templateSrv.replace(link.title || '', scopedVars);
+      info.target = link.targetBlank ? '_blank' : '';
     } else if (link.dashUri) {
       info.href = 'dashboard/' + link.dashUri + '?';
       info.title = this.templateSrv.replace(link.title || '', scopedVars);
diff --git a/public/app/features/panellinks/module.ts b/public/app/features/panellinks/module.ts
index 034e99f4296..66d4bd5b37f 100644
--- a/public/app/features/panellinks/module.ts
+++ b/public/app/features/panellinks/module.ts
@@ -39,7 +39,12 @@ export class PanelLinksEditorCtrl {
       backendSrv.search({ query: link.dashboard }).then(function(hits) {
         var dashboard = _.find(hits, { title: link.dashboard });
         if (dashboard) {
-          link.dashUri = dashboard.uri;
+          if (dashboard.url) {
+            link.url = dashboard.url;
+          } else {
+            // To support legacy url's
+            link.dashUri = dashboard.uri;
+          }
           link.title = dashboard.title;
         }
       });

From e97251fe28198055fa054e50ccd4c42d5ca6bd8e Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 8 Aug 2018 16:01:35 +0200
Subject: [PATCH 255/380] skip target _self to remove full page reload

---
 public/app/features/dashlinks/module.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/public/app/features/dashlinks/module.ts b/public/app/features/dashlinks/module.ts
index 6322e39f290..4d80f3632e6 100644
--- a/public/app/features/dashlinks/module.ts
+++ b/public/app/features/dashlinks/module.ts
@@ -145,6 +145,7 @@ export class DashLinksContainerCtrl {
               memo.push({
                 title: dash.title,
                 url: dash.url,
+                target: link.target === '_self' ? '' : link.target,
                 icon: 'fa fa-th-large',
                 keepTime: link.keepTime,
                 includeVars: link.includeVars,

From d7fb704e27daea9413b41d92b53075ec7f6b4b77 Mon Sep 17 00:00:00 2001
From: Pierre GIRAUD <pierre.giraud@gmail.com>
Date: Wed, 8 Aug 2018 15:51:13 +0200
Subject: [PATCH 256/380] Convert URL-like text to links in plugins readme

---
 public/app/features/plugins/plugin_edit_ctrl.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts
index 1244e6e38f7..6aa8b2bc38f 100644
--- a/public/app/features/plugins/plugin_edit_ctrl.ts
+++ b/public/app/features/plugins/plugin_edit_ctrl.ts
@@ -97,7 +97,9 @@ export class PluginEditCtrl {
 
   initReadme() {
     return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => {
-      var md = new Remarkable();
+      var md = new Remarkable({
+        linkify: true
+      });
       this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
     });
   }

From c1b9bbc2cf53447e39dddf0a58ae2b5c40c87ce8 Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Wed, 8 Aug 2018 16:50:30 +0200
Subject: [PATCH 257/380] Explore: Query hints for prometheus (#12833)

* Explore: Query hints for prometheus

- time series are analyzed on response
- hints are shown per query
- some hints have fixes
- fix rendered as link after hint
- click on fix executes the fix action

* Added tests for determineQueryHints()

* Fix index for rate hints in explore
---
 public/app/containers/Explore/Explore.tsx     | 107 +++++++++++++-----
 .../app/containers/Explore/PromQueryField.tsx |  46 ++++++--
 public/app/containers/Explore/QueryRows.tsx   |  25 +++-
 .../datasource/prometheus/datasource.ts       | 103 +++++++++++++++--
 .../prometheus/result_transformer.ts          |  22 ++--
 .../prometheus/specs/datasource.jest.ts       |  51 ++++++++-
 .../specs/result_transformer.jest.ts          |  12 +-
 public/sass/pages/_explore.scss               |   8 ++
 8 files changed, 305 insertions(+), 69 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 3ee5bceae8b..dcee963e2e7 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -19,6 +19,16 @@ import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
 
 const MAX_HISTORY_ITEMS = 100;
 
+function makeHints(hints) {
+  const hintsByIndex = [];
+  hints.forEach(hint => {
+    if (hint) {
+      hintsByIndex[hint.index] = hint;
+    }
+  });
+  return hintsByIndex;
+}
+
 function makeTimeSeriesList(dataList, options) {
   return dataList.map((seriesData, index) => {
     const datapoints = seriesData.datapoints || [];
@@ -37,7 +47,7 @@ function makeTimeSeriesList(dataList, options) {
   });
 }
 
-function parseInitialState(initial: string | undefined) {
+function parseUrlState(initial: string | undefined) {
   if (initial) {
     try {
       const parsed = JSON.parse(decodePathComponent(initial));
@@ -64,8 +74,9 @@ interface IExploreState {
   latency: number;
   loading: any;
   logsResult: any;
-  queries: any;
-  queryError: any;
+  queries: any[];
+  queryErrors: any[];
+  queryHints: any[];
   range: any;
   requestOptions: any;
   showingGraph: boolean;
@@ -82,7 +93,8 @@ export class Explore extends React.Component<any, IExploreState> {
 
   constructor(props) {
     super(props);
-    const { datasource, queries, range } = parseInitialState(props.routeParams.state);
+    const initialState: IExploreState = props.initialState;
+    const { datasource, queries, range } = parseUrlState(props.routeParams.state);
     this.state = {
       datasource: null,
       datasourceError: null,
@@ -95,7 +107,8 @@ export class Explore extends React.Component<any, IExploreState> {
       loading: false,
       logsResult: null,
       queries: ensureQueries(queries),
-      queryError: null,
+      queryErrors: [],
+      queryHints: [],
       range: range || { ...DEFAULT_RANGE },
       requestOptions: null,
       showingGraph: true,
@@ -105,7 +118,7 @@ export class Explore extends React.Component<any, IExploreState> {
       supportsLogs: null,
       supportsTable: null,
       tableResult: null,
-      ...props.initialState,
+      ...initialState,
     };
   }
 
@@ -191,6 +204,8 @@ export class Explore extends React.Component<any, IExploreState> {
       datasourceLoading: true,
       graphResult: null,
       logsResult: null,
+      queryErrors: [],
+      queryHints: [],
       tableResult: null,
     });
     const datasource = await this.props.datasourceSrv.get(option.value);
@@ -199,6 +214,7 @@ export class Explore extends React.Component<any, IExploreState> {
 
   onChangeQuery = (value: string, index: number, override?: boolean) => {
     const { queries } = this.state;
+    let { queryErrors, queryHints } = this.state;
     const prevQuery = queries[index];
     const edited = override ? false : prevQuery.query !== value;
     const nextQuery = {
@@ -208,7 +224,18 @@ export class Explore extends React.Component<any, IExploreState> {
     };
     const nextQueries = [...queries];
     nextQueries[index] = nextQuery;
-    this.setState({ queries: nextQueries }, override ? () => this.onSubmit() : undefined);
+    if (override) {
+      queryErrors = [];
+      queryHints = [];
+    }
+    this.setState(
+      {
+        queryErrors,
+        queryHints,
+        queries: nextQueries,
+      },
+      override ? () => this.onSubmit() : undefined
+    );
   };
 
   onChangeTime = nextRange => {
@@ -255,13 +282,32 @@ export class Explore extends React.Component<any, IExploreState> {
   };
 
   onClickTableCell = (columnKey: string, rowValue: string) => {
+    this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue });
+  };
+
+  onModifyQueries = (action: object, index?: number) => {
     const { datasource, queries } = this.state;
     if (datasource && datasource.modifyQuery) {
-      const nextQueries = queries.map(q => ({
-        ...q,
-        edited: false,
-        query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }),
-      }));
+      let nextQueries;
+      if (index === undefined) {
+        // Modify all queries
+        nextQueries = queries.map(q => ({
+          ...q,
+          edited: false,
+          query: datasource.modifyQuery(q.query, action),
+        }));
+      } else {
+        // Modify query only at index
+        nextQueries = [
+          ...queries.slice(0, index),
+          {
+            ...queries[index],
+            edited: false,
+            query: datasource.modifyQuery(queries[index].query, action),
+          },
+          ...queries.slice(index + 1),
+        ];
+      }
       this.setState({ queries: nextQueries }, () => this.onSubmit());
     }
   };
@@ -309,7 +355,7 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState({ history });
   }
 
-  buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
+  buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) {
     const { datasource, queries, range } = this.state;
     const resolution = this.el.offsetWidth;
     const absoluteRange = {
@@ -333,19 +379,20 @@ export class Explore extends React.Component<any, IExploreState> {
     if (!hasQuery(queries)) {
       return;
     }
-    this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
+    this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] });
     const now = Date.now();
-    const options = this.buildQueryOptions({ format: 'time_series', instant: false });
+    const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true });
     try {
       const res = await datasource.query(options);
       const result = makeTimeSeriesList(res.data, options);
+      const queryHints = res.hints ? makeHints(res.hints) : [];
       const latency = Date.now() - now;
-      this.setState({ latency, loading: false, graphResult: result, requestOptions: options });
+      this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options });
       this.onQuerySuccess(datasource.meta.id, queries);
     } catch (response) {
       console.error(response);
       const queryError = response.data ? response.data.error : response;
-      this.setState({ loading: false, queryError });
+      this.setState({ loading: false, queryErrors: [queryError] });
     }
   }
 
@@ -354,7 +401,7 @@ export class Explore extends React.Component<any, IExploreState> {
     if (!hasQuery(queries)) {
       return;
     }
-    this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
+    this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null });
     const now = Date.now();
     const options = this.buildQueryOptions({
       format: 'table',
@@ -369,7 +416,7 @@ export class Explore extends React.Component<any, IExploreState> {
     } catch (response) {
       console.error(response);
       const queryError = response.data ? response.data.error : response;
-      this.setState({ loading: false, queryError });
+      this.setState({ loading: false, queryErrors: [queryError] });
     }
   }
 
@@ -378,7 +425,7 @@ export class Explore extends React.Component<any, IExploreState> {
     if (!hasQuery(queries)) {
       return;
     }
-    this.setState({ latency: 0, loading: true, queryError: null, logsResult: null });
+    this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null });
     const now = Date.now();
     const options = this.buildQueryOptions({
       format: 'logs',
@@ -393,7 +440,7 @@ export class Explore extends React.Component<any, IExploreState> {
     } catch (response) {
       console.error(response);
       const queryError = response.data ? response.data.error : response;
-      this.setState({ loading: false, queryError });
+      this.setState({ loading: false, queryErrors: [queryError] });
     }
   }
 
@@ -415,7 +462,8 @@ export class Explore extends React.Component<any, IExploreState> {
       loading,
       logsResult,
       queries,
-      queryError,
+      queryErrors,
+      queryHints,
       range,
       requestOptions,
       showingGraph,
@@ -449,12 +497,12 @@ export class Explore extends React.Component<any, IExploreState> {
               </a>
             </div>
           ) : (
-              <div className="navbar-buttons explore-first-button">
-                <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
-                  Close Split
+            <div className="navbar-buttons explore-first-button">
+              <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
+                Close Split
               </button>
-              </div>
-            )}
+            </div>
+          )}
           {!datasourceMissing ? (
             <div className="navbar-buttons">
               <Select
@@ -504,14 +552,15 @@ export class Explore extends React.Component<any, IExploreState> {
             <QueryRows
               history={history}
               queries={queries}
+              queryErrors={queryErrors}
+              queryHints={queryHints}
               request={this.request}
               onAddQueryRow={this.onAddQueryRow}
               onChangeQuery={this.onChangeQuery}
+              onClickHintFix={this.onModifyQueries}
               onExecuteQuery={this.onSubmit}
               onRemoveQueryRow={this.onRemoveQueryRow}
             />
-            {queryError && !loading ? <div className="text-warning m-a-2">{queryError}</div> : null}
-
             <div className="result-options">
               {supportsGraph ? (
                 <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.onClickGraphButton}>
diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
index aab31584fd5..003f6345f13 100644
--- a/public/app/containers/Explore/PromQueryField.tsx
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -105,13 +105,16 @@ interface CascaderOption {
 }
 
 interface PromQueryFieldProps {
-  history?: any[];
+  error?: string;
+  hint?: any;
   histogramMetrics?: string[];
+  history?: any[];
   initialQuery?: string | null;
   labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
   labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
   metrics?: string[];
   metricsByPrefix?: CascaderOption[];
+  onClickHintFix?: (action: any) => void;
   onPressEnter?: () => void;
   onQueryChange?: (value: string, override?: boolean) => void;
   portalPrefix?: string;
@@ -189,6 +192,13 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     }
   };
 
+  onClickHintFix = () => {
+    const { hint, onClickHintFix } = this.props;
+    if (onClickHintFix && hint && hint.fix) {
+      onClickHintFix(hint.fix.action);
+    }
+  };
+
   onReceiveMetrics = () => {
     if (!this.state.metrics) {
       return;
@@ -435,6 +445,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
   }
 
   render() {
+    const { error, hint } = this.props;
     const { histogramMetrics, metricsByPrefix } = this.state;
     const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
     const metricsOptions = [
@@ -449,16 +460,29 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
             <button className="btn navbar-button navbar-button--tight">Metrics</button>
           </Cascader>
         </div>
-        <div className="slate-query-field-wrapper">
-          <TypeaheadField
-            additionalPlugins={this.plugins}
-            cleanText={cleanText}
-            initialValue={this.props.initialQuery}
-            onTypeahead={this.onTypeahead}
-            onWillApplySuggestion={willApplySuggestion}
-            onValueChanged={this.onChangeQuery}
-            placeholder="Enter a PromQL query"
-          />
+        <div className="prom-query-field-wrapper">
+          <div className="slate-query-field-wrapper">
+            <TypeaheadField
+              additionalPlugins={this.plugins}
+              cleanText={cleanText}
+              initialValue={this.props.initialQuery}
+              onTypeahead={this.onTypeahead}
+              onWillApplySuggestion={willApplySuggestion}
+              onValueChanged={this.onChangeQuery}
+              placeholder="Enter a PromQL query"
+            />
+          </div>
+          {error ? <div className="prom-query-field-info text-error">{error}</div> : null}
+          {hint ? (
+            <div className="prom-query-field-info text-warning">
+              {hint.label}{' '}
+              {hint.fix ? (
+                <a className="text-link muted" onClick={this.onClickHintFix}>
+                  {hint.fix.label}
+                </a>
+              ) : null}
+            </div>
+          ) : null}
         </div>
       </div>
     );
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx
index cd3cf23a927..a7d91d59033 100644
--- a/public/app/containers/Explore/QueryRows.tsx
+++ b/public/app/containers/Explore/QueryRows.tsx
@@ -1,5 +1,6 @@
 import React, { PureComponent } from 'react';
 
+// TODO make this datasource-plugin-dependent
 import QueryField from './PromQueryField';
 
 class QueryRow extends PureComponent<any, {}> {
@@ -21,6 +22,13 @@ class QueryRow extends PureComponent<any, {}> {
     this.onChangeQuery('', true);
   };
 
+  onClickHintFix = action => {
+    const { index, onClickHintFix } = this.props;
+    if (onClickHintFix) {
+      onClickHintFix(action, index);
+    }
+  };
+
   onClickRemoveButton = () => {
     const { index, onRemoveQueryRow } = this.props;
     if (onRemoveQueryRow) {
@@ -36,14 +44,17 @@ class QueryRow extends PureComponent<any, {}> {
   };
 
   render() {
-    const { edited, history, query, request } = this.props;
+    const { edited, history, query, queryError, queryHint, request } = this.props;
     return (
       <div className="query-row">
         <div className="query-row-field">
           <QueryField
+            error={queryError}
+            hint={queryHint}
             initialQuery={edited ? null : query}
             history={history}
             portalPrefix="explore"
+            onClickHintFix={this.onClickHintFix}
             onPressEnter={this.onPressEnter}
             onQueryChange={this.onChangeQuery}
             request={request}
@@ -67,11 +78,19 @@ class QueryRow extends PureComponent<any, {}> {
 
 export default class QueryRows extends PureComponent<any, {}> {
   render() {
-    const { className = '', queries, ...handlers } = this.props;
+    const { className = '', queries, queryErrors = [], queryHints = [], ...handlers } = this.props;
     return (
       <div className={className}>
         {queries.map((q, index) => (
-          <QueryRow key={q.key} index={index} query={q.query} edited={q.edited} {...handlers} />
+          <QueryRow
+            key={q.key}
+            index={index}
+            query={q.query}
+            queryError={queryErrors[index]}
+            queryHint={queryHints[index]}
+            edited={q.edited}
+            {...handlers}
+          />
         ))}
       </div>
     );
diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index fc8f3999856..ee5cc4375be 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -82,6 +82,68 @@ export function addLabelToQuery(query: string, key: string, value: string): stri
   return parts.join('');
 }
 
+export function determineQueryHints(series: any[]): any[] {
+  const hints = series.map((s, i) => {
+    const query: string = s.query;
+    const index: number = s.responseIndex;
+    if (query === undefined || index === undefined) {
+      return null;
+    }
+
+    // ..._bucket metric needs a histogram_quantile()
+    const histogramMetric = query.trim().match(/^\w+_bucket$/);
+    if (histogramMetric) {
+      const label = 'Time series has buckets, you probably wanted a histogram.';
+      return {
+        index,
+        label,
+        fix: {
+          label: 'Fix by adding histogram_quantile().',
+          action: {
+            type: 'ADD_HISTOGRAM_QUANTILE',
+            query,
+            index,
+          },
+        },
+      };
+    }
+
+    // Check for monotony
+    const datapoints: [number, number][] = s.datapoints;
+    const simpleMetric = query.trim().match(/^\w+$/);
+    if (simpleMetric && datapoints.length > 1) {
+      let increasing = false;
+      const monotonic = datapoints.every((dp, index) => {
+        if (index === 0) {
+          return true;
+        }
+        increasing = increasing || dp[0] > datapoints[index - 1][0];
+        // monotonic?
+        return dp[0] >= datapoints[index - 1][0];
+      });
+      if (increasing && monotonic) {
+        const label = 'Time series is monotonously increasing.';
+        return {
+          label,
+          index,
+          fix: {
+            label: 'Fix by adding rate().',
+            action: {
+              type: 'ADD_RATE',
+              query,
+              index,
+            },
+          },
+        };
+      }
+    }
+
+    // No hint found
+    return null;
+  });
+  return hints;
+}
+
 export function prometheusRegularEscape(value) {
   if (typeof value === 'string') {
     return value.replace(/'/g, "\\\\'");
@@ -223,10 +285,15 @@ export class PrometheusDatasource {
 
     return this.$q.all(allQueryPromise).then(responseList => {
       let result = [];
+      let hints = [];
 
       _.each(responseList, (response, index) => {
         if (response.status === 'error') {
-          throw response.error;
+          const error = {
+            index,
+            ...response.error,
+          };
+          throw error;
         }
 
         // Keeping original start/end for transformers
@@ -241,16 +308,24 @@ export class PrometheusDatasource {
           responseIndex: index,
           refId: activeTargets[index].refId,
         };
-        this.resultTransformer.transform(result, response, transformerOptions);
+        const series = this.resultTransformer.transform(response, transformerOptions);
+        result = [...result, ...series];
+
+        if (queries[index].hinting) {
+          const queryHints = determineQueryHints(series);
+          hints = [...hints, ...queryHints];
+        }
       });
 
-      return { data: result };
+      return { data: result, hints };
     });
   }
 
   createQuery(target, options, start, end) {
-    var query: any = {};
-    query.instant = target.instant;
+    const query: any = {
+      hinting: target.hinting,
+      instant: target.instant,
+    };
     var range = Math.ceil(end - start);
 
     var interval = kbn.interval_to_seconds(options.interval);
@@ -450,12 +525,20 @@ export class PrometheusDatasource {
     return state;
   }
 
-  modifyQuery(query: string, options: any): string {
-    const { addFilter } = options;
-    if (addFilter) {
-      return addLabelToQuery(query, addFilter.key, addFilter.value);
+  modifyQuery(query: string, action: any): string {
+    switch (action.type) {
+      case 'ADD_FILTER': {
+        return addLabelToQuery(query, action.key, action.value);
+      }
+      case 'ADD_HISTOGRAM_QUANTILE': {
+        return `histogram_quantile(0.95, sum(rate(${query}[5m])) by (le))`;
+      }
+      case 'ADD_RATE': {
+        return `rate(${query}[5m])`;
+      }
+      default:
+        return query;
     }
-    return query;
   }
 
   getPrometheusTime(date, roundUp) {
diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts
index a7c6703c10f..7cb160e2d8c 100644
--- a/public/app/plugins/datasource/prometheus/result_transformer.ts
+++ b/public/app/plugins/datasource/prometheus/result_transformer.ts
@@ -4,11 +4,11 @@ import TableModel from 'app/core/table_model';
 export class ResultTransformer {
   constructor(private templateSrv) {}
 
-  transform(result: any, response: any, options: any) {
+  transform(response: any, options: any): any[] {
     let prometheusResult = response.data.data.result;
 
     if (options.format === 'table') {
-      result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId));
+      return [this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)];
     } else if (options.format === 'heatmap') {
       let seriesList = [];
       prometheusResult.sort(sortSeriesByLabel);
@@ -16,16 +16,19 @@ export class ResultTransformer {
         seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
       }
       seriesList = this.transformToHistogramOverTime(seriesList);
-      result.push(...seriesList);
+      return seriesList;
     } else {
+      let seriesList = [];
       for (let metricData of prometheusResult) {
         if (response.data.data.resultType === 'matrix') {
-          result.push(this.transformMetricData(metricData, options, options.start, options.end));
+          seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
         } else if (response.data.data.resultType === 'vector') {
-          result.push(this.transformInstantMetricData(metricData, options));
+          seriesList.push(this.transformInstantMetricData(metricData, options));
         }
       }
+      return seriesList;
     }
+    return [];
   }
 
   transformMetricData(metricData, options, start, end) {
@@ -60,7 +63,12 @@ export class ResultTransformer {
       dps.push([null, t]);
     }
 
-    return { target: metricLabel, datapoints: dps };
+    return {
+      datapoints: dps,
+      query: options.query,
+      responseIndex: options.responseIndex,
+      target: metricLabel,
+    };
   }
 
   transformMetricDataToTable(md, resultCount: number, refId: string) {
@@ -124,7 +132,7 @@ export class ResultTransformer {
       metricLabel = null;
     metricLabel = this.createMetricLabel(md.metric, options);
     dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
-    return { target: metricLabel, datapoints: dps };
+    return { target: metricLabel, datapoints: dps, labels: md.metric };
   }
 
   createMetricLabel(labelData, options) {
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index b946a6f5e7e..6b3418ee6f9 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -3,6 +3,7 @@ import moment from 'moment';
 import q from 'q';
 import {
   alignRange,
+  determineQueryHints,
   PrometheusDatasource,
   prometheusSpecialRegexEscape,
   prometheusRegularEscape,
@@ -122,7 +123,7 @@ describe('PrometheusDatasource', () => {
       ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock);
       return ctx.ds.query(ctx.query).then(result => {
         let results = result.data;
-        return expect(results).toEqual(expected);
+        return expect(results).toMatchObject(expected);
       });
     });
 
@@ -180,6 +181,54 @@ describe('PrometheusDatasource', () => {
     });
   });
 
+  describe('determineQueryHints()', () => {
+    it('returns no hints for no series', () => {
+      expect(determineQueryHints([])).toEqual([]);
+    });
+
+    it('returns no hints for empty series', () => {
+      expect(determineQueryHints([{ datapoints: [], query: '' }])).toEqual([null]);
+    });
+
+    it('returns no hint for a monotonously decreasing series', () => {
+      const series = [{ datapoints: [[23, 1000], [22, 1001]], query: 'metric', responseIndex: 0 }];
+      const hints = determineQueryHints(series);
+      expect(hints).toEqual([null]);
+    });
+
+    it('returns a rate hint for a monotonously increasing series', () => {
+      const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'metric', responseIndex: 0 }];
+      const hints = determineQueryHints(series);
+      expect(hints.length).toBe(1);
+      expect(hints[0]).toMatchObject({
+        label: 'Time series is monotonously increasing.',
+        index: 0,
+        fix: {
+          action: {
+            type: 'ADD_RATE',
+            query: 'metric',
+          },
+        },
+      });
+    });
+
+    it('returns a histogram hint for a bucket series', () => {
+      const series = [{ datapoints: [[23, 1000]], query: 'metric_bucket', responseIndex: 0 }];
+      const hints = determineQueryHints(series);
+      expect(hints.length).toBe(1);
+      expect(hints[0]).toMatchObject({
+        label: 'Time series has buckets, you probably wanted a histogram.',
+        index: 0,
+        fix: {
+          action: {
+            type: 'ADD_HISTOGRAM_QUANTILE',
+            query: 'metric_bucket',
+          },
+        },
+      });
+    });
+  });
+
   describe('Prometheus regular escaping', () => {
     it('should not escape non-string', () => {
       expect(prometheusRegularEscape(12)).toEqual(12);
diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts
index e2a21a8f866..68224121414 100644
--- a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts
@@ -111,7 +111,6 @@ describe('Prometheus Result Transformer', () => {
     };
 
     it('should convert cumulative histogram to regular', () => {
-      let result = [];
       let options = {
         format: 'heatmap',
         start: 1445000010,
@@ -119,7 +118,7 @@ describe('Prometheus Result Transformer', () => {
         legendFormat: '{{le}}',
       };
 
-      ctx.resultTransformer.transform(result, { data: response }, options);
+      const result = ctx.resultTransformer.transform({ data: response }, options);
       expect(result).toEqual([
         { target: '1', datapoints: [[10, 1445000010000], [10, 1445000020000], [0, 1445000030000]] },
         { target: '2', datapoints: [[10, 1445000010000], [0, 1445000020000], [30, 1445000030000]] },
@@ -172,14 +171,13 @@ describe('Prometheus Result Transformer', () => {
           ],
         },
       };
-      let result = [];
       let options = {
         format: 'timeseries',
         start: 0,
         end: 2,
       };
 
-      ctx.resultTransformer.transform(result, { data: response }, options);
+      const result = ctx.resultTransformer.transform({ data: response }, options);
       expect(result).toEqual([{ target: 'test{job="testjob"}', datapoints: [[10, 0], [10, 1000], [0, 2000]] }]);
     });
 
@@ -196,7 +194,6 @@ describe('Prometheus Result Transformer', () => {
           ],
         },
       };
-      let result = [];
       let options = {
         format: 'timeseries',
         step: 1,
@@ -204,7 +201,7 @@ describe('Prometheus Result Transformer', () => {
         end: 2,
       };
 
-      ctx.resultTransformer.transform(result, { data: response }, options);
+      const result = ctx.resultTransformer.transform({ data: response }, options);
       expect(result).toEqual([{ target: 'test{job="testjob"}', datapoints: [[null, 0], [10, 1000], [0, 2000]] }]);
     });
 
@@ -221,7 +218,6 @@ describe('Prometheus Result Transformer', () => {
           ],
         },
       };
-      let result = [];
       let options = {
         format: 'timeseries',
         step: 2,
@@ -229,7 +225,7 @@ describe('Prometheus Result Transformer', () => {
         end: 8,
       };
 
-      ctx.resultTransformer.transform(result, { data: response }, options);
+      const result = ctx.resultTransformer.transform({ data: response }, options);
       expect(result).toEqual([
         { target: 'test{job="testjob"}', datapoints: [[null, 0], [null, 2000], [10, 4000], [null, 6000], [10, 8000]] },
       ]);
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index 6d7ce5a6b61..903193d8b10 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -158,4 +158,12 @@
   .prom-query-field {
     display: flex;
   }
+
+  .prom-query-field-wrapper {
+    width: 100%;
+  }
+
+  .prom-query-field-info {
+    margin: 0.25em 0.5em 0.5em;
+  }
 }

From 128a5d98e11f6418c2984c000f1a571139526f3b Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Wed, 8 Aug 2018 16:56:21 +0200
Subject: [PATCH 258/380] Explore: expand recording rules for queries

- load recording rules from prometheus
- map rule name to rule query
- query hint to detect recording rules in query
- click on hint fix expands rule name to query
---
 public/app/containers/Explore/Explore.tsx     |  4 +
 .../Explore/PromQueryField.jest.tsx           | 42 +++++++++-
 .../app/containers/Explore/PromQueryField.tsx | 20 ++++-
 .../datasource/prometheus/datasource.ts       | 77 ++++++++++++++++++-
 .../prometheus/specs/datasource.jest.ts       | 31 ++++++++
 5 files changed, 170 insertions(+), 4 deletions(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index dcee963e2e7..9620ac4f91b 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -169,6 +169,10 @@ export class Explore extends React.Component<any, IExploreState> {
     const historyKey = `grafana.explore.history.${datasourceId}`;
     const history = store.getObject(historyKey, []);
 
+    if (datasource.init) {
+      datasource.init();
+    }
+
     this.setState(
       {
         datasource,
diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx
index cd0d940961e..350a529c89e 100644
--- a/public/app/containers/Explore/PromQueryField.jest.tsx
+++ b/public/app/containers/Explore/PromQueryField.jest.tsx
@@ -3,7 +3,7 @@ import Enzyme, { shallow } from 'enzyme';
 import Adapter from 'enzyme-adapter-react-16';
 import Plain from 'slate-plain-serializer';
 
-import PromQueryField from './PromQueryField';
+import PromQueryField, { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField';
 
 Enzyme.configure({ adapter: new Adapter() });
 
@@ -177,3 +177,43 @@ describe('PromQueryField typeahead handling', () => {
     });
   });
 });
+
+describe('groupMetricsByPrefix()', () => {
+  it('returns an empty group for no metrics', () => {
+    expect(groupMetricsByPrefix([])).toEqual([]);
+  });
+
+  it('returns options grouped by prefix', () => {
+    expect(groupMetricsByPrefix(['foo_metric'])).toMatchObject([
+      {
+        value: 'foo',
+        children: [
+          {
+            value: 'foo_metric',
+          },
+        ],
+      },
+    ]);
+  });
+
+  it('returns options without prefix as toplevel option', () => {
+    expect(groupMetricsByPrefix(['metric'])).toMatchObject([
+      {
+        value: 'metric',
+      },
+    ]);
+  });
+
+  it('returns recording rules grouped separately', () => {
+    expect(groupMetricsByPrefix([':foo_metric:'])).toMatchObject([
+      {
+        value: RECORDING_RULES_GROUP,
+        children: [
+          {
+            value: ':foo_metric:',
+          },
+        ],
+      },
+    ]);
+  });
+});
diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
index 003f6345f13..1b3ff33971d 100644
--- a/public/app/containers/Explore/PromQueryField.tsx
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -28,6 +28,7 @@ const HISTORY_ITEM_COUNT = 5;
 const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
 const METRIC_MARK = 'metric';
 const PRISM_LANGUAGE = 'promql';
+export const RECORDING_RULES_GROUP = '__recording_rules__';
 
 export const wrapLabel = (label: string) => ({ label });
 export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
@@ -52,7 +53,22 @@ export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion
 }
 
 export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] {
-  return _.chain(metrics)
+  // Filter out recording rules and insert as first option
+  const ruleRegex = /:\w+:/;
+  const ruleNames = metrics.filter(metric => ruleRegex.test(metric));
+  const rulesOption = {
+    label: 'Recording rules',
+    value: RECORDING_RULES_GROUP,
+    children: ruleNames
+      .slice()
+      .sort()
+      .map(name => ({ label: name, value: name })),
+  };
+
+  const options = ruleNames.length > 0 ? [rulesOption] : [];
+
+  const metricsOptions = _.chain(metrics)
+    .filter(metric => !ruleRegex.test(metric))
     .groupBy(metric => metric.split(delimiter)[0])
     .map((metricsForPrefix: string[], prefix: string): CascaderOption => {
       const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
@@ -65,6 +81,8 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad
     })
     .sortBy('label')
     .value();
+
+  return [...options, ...metricsOptions];
 }
 
 export function willApplySuggestion(
diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index ee5cc4375be..ef440ab515d 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -82,7 +82,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri
   return parts.join('');
 }
 
-export function determineQueryHints(series: any[]): any[] {
+export function determineQueryHints(series: any[], datasource?: any): any[] {
   const hints = series.map((s, i) => {
     const query: string = s.query;
     const index: number = s.responseIndex;
@@ -138,12 +138,56 @@ export function determineQueryHints(series: any[]): any[] {
       }
     }
 
+    // Check for recording rules expansion
+    if (datasource && datasource.ruleMappings) {
+      const mapping = datasource.ruleMappings;
+      const mappingForQuery = Object.keys(mapping).reduce((acc, ruleName) => {
+        if (query.search(ruleName) > -1) {
+          return {
+            ...acc,
+            [ruleName]: mapping[ruleName],
+          };
+        }
+        return acc;
+      }, {});
+      if (_.size(mappingForQuery) > 0) {
+        const label = 'Query contains recording rules.';
+        return {
+          label,
+          index,
+          fix: {
+            label: 'Expand rules',
+            action: {
+              type: 'EXPAND_RULES',
+              query,
+              index,
+              mapping: mappingForQuery,
+            },
+          },
+        };
+      }
+    }
+
     // No hint found
     return null;
   });
   return hints;
 }
 
+export function extractRuleMappingFromGroups(groups: any[]) {
+  return groups.reduce(
+    (mapping, group) =>
+      group.rules.filter(rule => rule.type === 'recording').reduce(
+        (acc, rule) => ({
+          ...acc,
+          [rule.name]: rule.query,
+        }),
+        mapping
+      ),
+    {}
+  );
+}
+
 export function prometheusRegularEscape(value) {
   if (typeof value === 'string') {
     return value.replace(/'/g, "\\\\'");
@@ -162,6 +206,7 @@ export class PrometheusDatasource {
   type: string;
   editorSrc: string;
   name: string;
+  ruleMappings: { [index: string]: string };
   supportsExplore: boolean;
   supportMetrics: boolean;
   url: string;
@@ -189,6 +234,11 @@ export class PrometheusDatasource {
     this.queryTimeout = instanceSettings.jsonData.queryTimeout;
     this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
     this.resultTransformer = new ResultTransformer(templateSrv);
+    this.ruleMappings = {};
+  }
+
+  init() {
+    this.loadRules();
   }
 
   _request(url, data?, options?: any) {
@@ -312,7 +362,7 @@ export class PrometheusDatasource {
         result = [...result, ...series];
 
         if (queries[index].hinting) {
-          const queryHints = determineQueryHints(series);
+          const queryHints = determineQueryHints(series, this);
           hints = [...hints, ...queryHints];
         }
       });
@@ -525,6 +575,21 @@ export class PrometheusDatasource {
     return state;
   }
 
+  loadRules() {
+    this.metadataRequest('/api/v1/rules')
+      .then(res => res.data || res.json())
+      .then(body => {
+        const groups = _.get(body, ['data', 'groups']);
+        if (groups) {
+          this.ruleMappings = extractRuleMappingFromGroups(groups);
+        }
+      })
+      .catch(e => {
+        console.log('Rules API is experimental. Ignore next error.');
+        console.error(e);
+      });
+  }
+
   modifyQuery(query: string, action: any): string {
     switch (action.type) {
       case 'ADD_FILTER': {
@@ -536,6 +601,14 @@ export class PrometheusDatasource {
       case 'ADD_RATE': {
         return `rate(${query}[5m])`;
       }
+      case 'EXPAND_RULES': {
+        const mapping = action.mapping;
+        if (mapping) {
+          const ruleNames = Object.keys(mapping);
+          const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\()`, 'ig');
+          return query.replace(rulesRegex, (match, pre, name, post) => mapping[name]);
+        }
+      }
       default:
         return query;
     }
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index 6b3418ee6f9..a108909e6e1 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -4,6 +4,7 @@ import q from 'q';
 import {
   alignRange,
   determineQueryHints,
+  extractRuleMappingFromGroups,
   PrometheusDatasource,
   prometheusSpecialRegexEscape,
   prometheusRegularEscape,
@@ -229,6 +230,36 @@ describe('PrometheusDatasource', () => {
     });
   });
 
+  describe('extractRuleMappingFromGroups()', () => {
+    it('returns empty mapping for no rule groups', () => {
+      expect(extractRuleMappingFromGroups([])).toEqual({});
+    });
+
+    it('returns a mapping for recording rules only', () => {
+      const groups = [
+        {
+          rules: [
+            {
+              name: 'HighRequestLatency',
+              query: 'job:request_latency_seconds:mean5m{job="myjob"} > 0.5',
+              type: 'alerting',
+            },
+            {
+              name: 'job:http_inprogress_requests:sum',
+              query: 'sum(http_inprogress_requests) by (job)',
+              type: 'recording',
+            },
+          ],
+          file: '/rules.yaml',
+          interval: 60,
+          name: 'example',
+        },
+      ];
+      const mapping = extractRuleMappingFromGroups(groups);
+      expect(mapping).toEqual({ 'job:http_inprogress_requests:sum': 'sum(http_inprogress_requests) by (job)' });
+    });
+  });
+
   describe('Prometheus regular escaping', () => {
     it('should not escape non-string', () => {
       expect(prometheusRegularEscape(12)).toEqual(12);

From 62a25a4f287d5be0d0afa3e8e5497c3656531430 Mon Sep 17 00:00:00 2001
From: Wade Robson <wade.robson4@gmail.com>
Date: Wed, 8 Aug 2018 09:13:28 -0700
Subject: [PATCH 259/380] Add example OR search_filter to docs

---
 docs/sources/installation/ldap.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md
index 9a381b9e467..f01bf717b34 100644
--- a/docs/sources/installation/ldap.md
+++ b/docs/sources/installation/ldap.md
@@ -48,6 +48,7 @@ bind_dn = "cn=admin,dc=grafana,dc=org"
 bind_password = 'grafana'
 
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
+# Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))"
 search_filter = "(cn=%s)"
 
 # An array of base dns to search through

From a5d1fb7e568642e2fc30331411f61bd959eb9801 Mon Sep 17 00:00:00 2001
From: Lorenz Brun <lorenz@dolansoft.org>
Date: Mon, 29 Jan 2018 12:01:05 +0100
Subject: [PATCH 260/380] Simple Docker-based build option

This Dockerfile allows anyone with a recent version of Docker to quickly build a fully working Grafana container without any local build tooling. Pulling the sources from Git and calling `docker build .` is enough.
---
 Dockerfile | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 Dockerfile

diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000000..7b8402bfe9c
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,28 @@
+FROM golang:1.9
+RUN go get -u github.com/golang/dep/cmd/dep
+WORKDIR $GOPATH/src/github.com/grafana/grafana
+COPY Gopkg.toml Gopkg.lock ./
+RUN dep ensure --vendor-only
+COPY pkg pkg
+RUN go install ./pkg/cmd/grafana-server
+RUN go install ./pkg/cmd/grafana-cli
+RUN strip $GOPATH/bin/grafana-server
+RUN strip $GOPATH/bin/grafana-cli
+
+FROM node:8
+WORKDIR /usr/src/app/
+COPY package.json yarn.lock ./
+RUN yarn install --frozen-lockfile
+ENV NODE_ENV production
+COPY . ./
+RUN yarn run build
+
+FROM debian:stretch-slim
+WORKDIR /app
+ENV PATH $PATH:/app/bin
+COPY --from=0 /go/bin/grafana-server ./bin/
+COPY --from=0 /go/bin/grafana-cli ./bin/
+COPY --from=1 /usr/src/app/public ./public
+COPY --from=1 /usr/src/app/tools ./tools
+COPY conf ./conf
+CMD ["grafana-server"]

From c89f21ba2951071c4cf539269391bc7199035099 Mon Sep 17 00:00:00 2001
From: Lorenz Brun <lorenz@dolansoft.org>
Date: Wed, 7 Feb 2018 01:58:37 +0100
Subject: [PATCH 261/380] More efficient builds and some fixes to the Go
 binaries

---
 Dockerfile | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 7b8402bfe9c..9a15b4974f5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,24 +4,24 @@ WORKDIR $GOPATH/src/github.com/grafana/grafana
 COPY Gopkg.toml Gopkg.lock ./
 RUN dep ensure --vendor-only
 COPY pkg pkg
-RUN go install ./pkg/cmd/grafana-server
-RUN go install ./pkg/cmd/grafana-cli
-RUN strip $GOPATH/bin/grafana-server
-RUN strip $GOPATH/bin/grafana-cli
+RUN go install -ldflags="-s -w" ./pkg/cmd/grafana-server
+RUN go install -ldflags="-s -w" ./pkg/cmd/grafana-cli
 
 FROM node:8
 WORKDIR /usr/src/app/
 COPY package.json yarn.lock ./
 RUN yarn install --frozen-lockfile
 ENV NODE_ENV production
-COPY . ./
+COPY Gruntfile.js tsconfig.json tslint.json ./
+COPY public public
+COPY scripts scripts
+COPY emails emails
 RUN yarn run build
 
 FROM debian:stretch-slim
 WORKDIR /app
 ENV PATH $PATH:/app/bin
-COPY --from=0 /go/bin/grafana-server ./bin/
-COPY --from=0 /go/bin/grafana-cli ./bin/
+COPY --from=0 /go/bin/grafana-server /go/bin/grafana-cli ./bin/
 COPY --from=1 /usr/src/app/public ./public
 COPY --from=1 /usr/src/app/tools ./tools
 COPY conf ./conf

From 96d6657b00495afa62d25e18300ab93da0a6745a Mon Sep 17 00:00:00 2001
From: Dan Cech <dcech@grafana.com>
Date: Fri, 20 Apr 2018 14:28:52 -0400
Subject: [PATCH 262/380] produce an image compatible with grafana-docker

---
 .dockerignore |  4 +++
 Dockerfile    | 73 ++++++++++++++++++++++++++++++++++++++++++---------
 Makefile      |  3 +++
 docker/run.sh | 67 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 134 insertions(+), 13 deletions(-)
 create mode 100644 docker/run.sh

diff --git a/.dockerignore b/.dockerignore
index e50dfd86aa3..296b86db001 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,9 +3,12 @@
 .git
 .gitignore
 .github
+.vscode
+bin
 data*
 dist
 docker
+Dockerfile
 docs
 dump.rdb
 node_modules
@@ -13,3 +16,4 @@ node_modules
 /tmp
 *.yml
 *.md
+/tmp
diff --git a/Dockerfile b/Dockerfile
index 9a15b4974f5..39bc13f4c6f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,28 +1,75 @@
-FROM golang:1.9
-RUN go get -u github.com/golang/dep/cmd/dep
-WORKDIR $GOPATH/src/github.com/grafana/grafana
-COPY Gopkg.toml Gopkg.lock ./
-RUN dep ensure --vendor-only
-COPY pkg pkg
-RUN go install -ldflags="-s -w" ./pkg/cmd/grafana-server
-RUN go install -ldflags="-s -w" ./pkg/cmd/grafana-cli
+# Golang build container
+FROM golang:1.10
 
+WORKDIR $GOPATH/src/github.com/grafana/grafana
+
+COPY Gopkg.toml Gopkg.lock ./
+COPY vendor vendor
+
+ARG DEP_ENSURE=""
+RUN if [ ! -z "${DEP_ENSURE}" ]; then \
+      go get -u github.com/golang/dep/cmd/dep && \
+      dep ensure --vendor-only; \
+    fi
+
+COPY pkg pkg
+RUN go install -ldflags="-s -w" ./pkg/cmd/grafana-server && \
+    go install -ldflags="-s -w" ./pkg/cmd/grafana-cli
+
+# Node build container
 FROM node:8
+
 WORKDIR /usr/src/app/
+
 COPY package.json yarn.lock ./
 RUN yarn install --frozen-lockfile
-ENV NODE_ENV production
+
 COPY Gruntfile.js tsconfig.json tslint.json ./
 COPY public public
 COPY scripts scripts
 COPY emails emails
+
+ENV NODE_ENV production
 RUN yarn run build
 
+# Final container
 FROM debian:stretch-slim
-WORKDIR /app
-ENV PATH $PATH:/app/bin
+
+ARG GF_UID="472"
+ARG GF_GID="472"
+
+ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
+    GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
+    GF_PATHS_DATA="/var/lib/grafana" \
+    GF_PATHS_HOME="/usr/share/grafana" \
+    GF_PATHS_LOGS="/var/log/grafana" \
+    GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
+    GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
+
+WORKDIR $GF_PATHS_HOME
+
+COPY conf ./conf
+
+RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
+    groupadd -r -g $GF_GID grafana && \
+    useradd -r -u $GF_UID -g grafana grafana && \
+    mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
+             "$GF_PATHS_PROVISIONING/dashboards" \
+             "$GF_PATHS_LOGS" \
+             "$GF_PATHS_PLUGINS" \
+             "$GF_PATHS_DATA" && \
+    cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
+    cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
+    chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
+    chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
+
 COPY --from=0 /go/bin/grafana-server /go/bin/grafana-cli ./bin/
 COPY --from=1 /usr/src/app/public ./public
 COPY --from=1 /usr/src/app/tools ./tools
-COPY conf ./conf
-CMD ["grafana-server"]
+
+EXPOSE 3000
+
+COPY ./docker/run.sh /run.sh
+
+USER grafana
+ENTRYPOINT [ "/run.sh" ]
diff --git a/Makefile b/Makefile
index 9e136688eb7..c6915409ed7 100644
--- a/Makefile
+++ b/Makefile
@@ -30,6 +30,9 @@ build-docker-dev:
 	cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
 	cd packaging/docker && docker build --tag grafana/grafana:dev .
 
+build-docker-full:
+	docker build --tag grafana/grafana:dev .
+
 test-go:
 	go test -v ./pkg/...
 
diff --git a/docker/run.sh b/docker/run.sh
new file mode 100644
index 00000000000..df64ce3adf4
--- /dev/null
+++ b/docker/run.sh
@@ -0,0 +1,67 @@
+#!/bin/bash -e
+
+PERMISSIONS_OK=0
+
+if [ ! -r "$GF_PATHS_CONFIG" ]; then
+    echo "GF_PATHS_CONFIG='$GF_PATHS_CONFIG' is not readable."
+    PERMISSIONS_OK=1
+fi
+
+if [ ! -w "$GF_PATHS_DATA" ]; then
+    echo "GF_PATHS_DATA='$GF_PATHS_DATA' is not writable."
+    PERMISSIONS_OK=1
+fi
+
+if [ ! -r "$GF_PATHS_HOME" ]; then
+    echo "GF_PATHS_HOME='$GF_PATHS_HOME' is not readable."
+    PERMISSIONS_OK=1
+fi
+
+if [ $PERMISSIONS_OK -eq 1 ]; then
+    echo "You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later"
+fi
+
+if [ ! -d "$GF_PATHS_PLUGINS" ]; then
+    mkdir "$GF_PATHS_PLUGINS"
+fi
+
+
+if [ ! -z ${GF_AWS_PROFILES+x} ]; then
+    > "$GF_PATHS_HOME/.aws/credentials"
+
+    for profile in ${GF_AWS_PROFILES}; do
+        access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID"
+        secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY"
+        region_varname="GF_AWS_${profile}_REGION"
+
+        if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then
+            echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials"
+            echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            if [ ! -z "${!region_varname}" ]; then
+                echo "region = ${!region_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
+            fi
+        fi
+    done
+
+    chmod 600 "$GF_PATHS_HOME/.aws/credentials"
+fi
+
+if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then
+  OLDIFS=$IFS
+  IFS=','
+  for plugin in ${GF_INSTALL_PLUGINS}; do
+    IFS=$OLDIFS
+    grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}
+  done
+fi
+
+exec grafana-server                                         \
+  --homepath="$GF_PATHS_HOME"                               \
+  --config="$GF_PATHS_CONFIG"                               \
+  "$@"                                                      \
+  cfg:default.log.mode="console"                            \
+  cfg:default.paths.data="$GF_PATHS_DATA"                   \
+  cfg:default.paths.logs="$GF_PATHS_LOGS"                   \
+  cfg:default.paths.plugins="$GF_PATHS_PLUGINS"             \
+  cfg:default.paths.provisioning="$GF_PATHS_PROVISIONING"
\ No newline at end of file

From c0254905184203a4ebb3071921e2c5151338cb8a Mon Sep 17 00:00:00 2001
From: Dan Cech <dcech@grafana.com>
Date: Tue, 8 May 2018 12:14:38 -0400
Subject: [PATCH 263/380] move run script, update README

---
 Dockerfile                        |  2 +-
 README.md                         | 32 +++++++++++++++++++++----------
 {docker => scripts/docker}/run.sh |  0
 3 files changed, 23 insertions(+), 11 deletions(-)
 rename {docker => scripts/docker}/run.sh (100%)
 mode change 100644 => 100755

diff --git a/Dockerfile b/Dockerfile
index 39bc13f4c6f..0881d66803d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -69,7 +69,7 @@ COPY --from=1 /usr/src/app/tools ./tools
 
 EXPOSE 3000
 
-COPY ./docker/run.sh /run.sh
+COPY ./scripts/docker/run.sh /run.sh
 
 USER grafana
 ENTRYPOINT [ "/run.sh" ]
diff --git a/README.md b/README.md
index b2baf0ece59..d6083bb1504 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ To build the assets, rebuild on file change, and serve them by Grafana's webserv
 ```bash
 npm install -g yarn
 yarn install --pure-lockfile
-npm run watch
+yarn run watch
 ```
 
 Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
@@ -54,14 +54,14 @@ env GRAFANA_THEME=light yarn start
 ```
 Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.
 
-Run tests 
+Run tests
 ```bash
-npm run jest
+yarn run jest
 ```
 
 Run karma tests
 ```bash
-npm run karma
+yarn run karma
 ```
 
 ### Recompile backend on source change
@@ -98,30 +98,42 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
 #### Frontend
 Execute all frontend tests
 ```bash
-npm run test
+yarn run test
 ```
 
 Writing & watching frontend tests (we have two test runners)
 
 - jest for all new tests that do not require browser context (React+more)
-   - Start watcher: `npm run jest`
+   - Start watcher: `yarn run jest`
    - Jest will run all test files that end with the name ".jest.ts"
 - karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
-  - Start watcher: `npm run karma`
+  - Start watcher: `yarn run karma`
   - Karma+Mocha runs all files that end with the name "_specs.ts".
 
 #### Backend
 ```bash
 # Run Golang tests using sqlite3 as database (default)
-go test ./pkg/... 
+go test ./pkg/...
 
 # Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests
-GRAFANA_TEST_DB=mysql go test ./pkg/... 
+GRAFANA_TEST_DB=mysql go test ./pkg/...
 
 # Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests
-GRAFANA_TEST_DB=postgres go test ./pkg/... 
+GRAFANA_TEST_DB=postgres go test ./pkg/...
 ```
 
+## Building custom docker image
+
+You can build a custom image using Docker, which doesn't require installing any dependencies besides docker itself.
+```bash
+git clone https://github.com/grafana/grafana
+cd grafana
+docker build -t grafana:dev .
+docker run -d --name=grafana -p 3000:3000 grafana:dev
+```
+
+Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
+
 ## Contribute
 
 If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
diff --git a/docker/run.sh b/scripts/docker/run.sh
old mode 100644
new mode 100755
similarity index 100%
rename from docker/run.sh
rename to scripts/docker/run.sh

From 8d0a100b941e3eb485760f460bca32a2ee3a4dd0 Mon Sep 17 00:00:00 2001
From: Dan Cech <dcech@grafana.com>
Date: Wed, 30 May 2018 11:39:43 -0400
Subject: [PATCH 264/380] remove duplicated /tmp entry in .dockerignore

---
 .dockerignore | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.dockerignore b/.dockerignore
index 296b86db001..c535fa427b5 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -16,4 +16,3 @@ node_modules
 /tmp
 *.yml
 *.md
-/tmp

From d48f1f57f0d40ff17c9ece91683827d69fdaee4b Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Tue, 7 Aug 2018 16:11:37 +0200
Subject: [PATCH 265/380] build: fixes png rendering in the docker based
 docker-image build.

---
 Dockerfile                  | 19 +++++++++++++------
 packaging/docker/Dockerfile |  3 ++-
 2 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 0881d66803d..f7e45893c38 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,8 +13,10 @@ RUN if [ ! -z "${DEP_ENSURE}" ]; then \
     fi
 
 COPY pkg pkg
-RUN go install -ldflags="-s -w" ./pkg/cmd/grafana-server && \
-    go install -ldflags="-s -w" ./pkg/cmd/grafana-cli
+COPY build.go build.go
+COPY package.json package.json
+
+RUN go run build.go build
 
 # Node build container
 FROM node:8
@@ -22,7 +24,7 @@ FROM node:8
 WORKDIR /usr/src/app/
 
 COPY package.json yarn.lock ./
-RUN yarn install --frozen-lockfile
+RUN yarn install --pure-lockfile --no-progress
 
 COPY Gruntfile.js tsconfig.json tslint.json ./
 COPY public public
@@ -30,7 +32,7 @@ COPY scripts scripts
 COPY emails emails
 
 ENV NODE_ENV production
-RUN yarn run build
+RUN ./node_modules/.bin/grunt build
 
 # Final container
 FROM debian:stretch-slim
@@ -48,6 +50,10 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
 
 WORKDIR $GF_PATHS_HOME
 
+RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/*
+
 COPY conf ./conf
 
 RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
@@ -63,13 +69,14 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
     chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
     chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
 
-COPY --from=0 /go/bin/grafana-server /go/bin/grafana-cli ./bin/
+COPY --from=0 /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-server /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-cli ./bin/
 COPY --from=1 /usr/src/app/public ./public
 COPY --from=1 /usr/src/app/tools ./tools
+COPY tools/phantomjs/render.js ./tools/phantomjs/render.js
 
 EXPOSE 3000
 
-COPY ./scripts/docker/run.sh /run.sh
+COPY ./packaging/docker/run.sh /run.sh
 
 USER grafana
 ENTRYPOINT [ "/run.sh" ]
diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile
index e2109b74909..890d6a4fb11 100644
--- a/packaging/docker/Dockerfile
+++ b/packaging/docker/Dockerfile
@@ -23,6 +23,8 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
     GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
     GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
 
+WORKDIR $GF_PATHS_HOME
+
 RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
     apt-get autoremove -y && \
     rm -rf /var/lib/apt/lists/*
@@ -47,5 +49,4 @@ EXPOSE 3000
 COPY ./run.sh /run.sh
 
 USER grafana
-WORKDIR /
 ENTRYPOINT [ "/run.sh" ]

From b987aee7cffe873a01380b4ed6dbf60838f97736 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Sat, 21 Jul 2018 20:00:26 +0200
Subject: [PATCH 266/380] add timescaledb option to postgres datasource

This adds an option to the postgres datasource config for
timescaledb support. When set to auto it will check for
timescaledb when testing the datasource.

When this option is enabled the $__timeGroup macro will
use the time_bucket function from timescaledb to group
times by an interval.

This also passes the datasource edit control to testDatasource
to allow for setting additional settings, this might be useful
for other datasources aswell which have optional or version
dependant features which can be queried.
---
 pkg/tsdb/postgres/macros.go                   |  8 +++-
 pkg/tsdb/postgres/macros_test.go              | 22 +++++++++-
 pkg/tsdb/postgres/postgres_test.go            | 23 ++++++++++-
 public/app/features/plugins/ds_edit_ctrl.ts   |  2 +-
 .../plugins/datasource/postgres/datasource.ts | 40 +++++++++----------
 .../datasource/postgres/partials/config.html  | 14 +++++++
 6 files changed, 85 insertions(+), 24 deletions(-)

diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index aebdc55d1d7..4f1d3f72558 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -130,13 +130,19 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 				m.query.Model.Set("fillValue", floatVal)
 			}
 		}
-		return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+
+		if m.query.DataSource.JsonData.Get("timescaledb").MustString("auto") == "enabled" {
+			return fmt.Sprintf("time_bucket('%vs',%s) AS time", interval.Seconds(), args[0]), nil
+		} else {
+			return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
+		}
 	case "__timeGroupAlias":
 		tg, err := m.evaluateMacro("__timeGroup", args)
 		if err == nil {
 			return tg + " AS \"time\"", err
 		}
 		return "", err
+
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index beeea93893b..6c4ba8305b1 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -6,6 +6,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -13,7 +15,9 @@ import (
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
 		engine := newPostgresMacroEngine()
-		query := &tsdb.Query{}
+		query := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}}
+		queryTS := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}}
+		queryTS.DataSource.JsonData.Set("timescaledb", "enabled")
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
 			from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
@@ -83,6 +87,22 @@ func TestMacroEngine(t *testing.T) {
 				So(sql2, ShouldEqual, sql+" AS \"time\"")
 			})
 
+			Convey("interpolate __timeGroup function with TimescaleDB enabled", func() {
+
+				sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column) AS time")
+			})
+
+			Convey("interpolate __timeGroup function with spaces between args and TimescaleDB enabled", func() {
+
+				sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column) AS time")
+			})
+
 			Convey("interpolate __timeTo function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 				So(err, ShouldBeNil)
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index 9e363529df1..27888b318a9 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -27,7 +27,7 @@ import (
 // use to verify that the generated data are vizualized as expected, see
 // devenv/README.md for setup instructions.
 func TestPostgres(t *testing.T) {
-	// change to true to run the MySQL tests
+	// change to true to run the PostgreSQL tests
 	runPostgresTests := false
 	// runPostgresTests := true
 
@@ -102,6 +102,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": "SELECT * FROM postgres_types",
 								"format": "table",
@@ -182,6 +183,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
@@ -226,6 +228,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
@@ -280,6 +283,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
@@ -401,6 +405,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeInt64" as time, "timeInt64" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -423,6 +428,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeInt64Nullable" as time, "timeInt64Nullable" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -445,6 +451,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeFloat64" as time, "timeFloat64" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -467,6 +474,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeFloat64Nullable" as time, "timeFloat64Nullable" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -511,6 +519,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeInt32Nullable" as time, "timeInt32Nullable" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -533,6 +542,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeFloat32" as time, "timeFloat32" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -555,6 +565,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeFloat32Nullable" as time, "timeFloat32Nullable" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -577,6 +588,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT $__timeEpoch(time), measurement || ' - value one' as metric, "valueOne" FROM metric_values ORDER BY 1`,
 								"format": "time_series",
@@ -625,6 +637,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT $__timeEpoch(time), "valueOne", "valueTwo" FROM metric_values ORDER BY 1`,
 								"format": "time_series",
@@ -682,6 +695,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`,
 								"format": "table",
@@ -705,6 +719,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`,
 								"format": "table",
@@ -731,6 +746,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": fmt.Sprintf(`SELECT
 									CAST('%s' AS TIMESTAMP) as time,
@@ -761,6 +777,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": fmt.Sprintf(`SELECT
 									 %d as time,
@@ -791,6 +808,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": fmt.Sprintf(`SELECT
 									 cast(%d as bigint) as time,
@@ -821,6 +839,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": fmt.Sprintf(`SELECT
 									 %d as time,
@@ -849,6 +868,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT
 									 cast(null as bigint) as time,
@@ -877,6 +897,7 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
+							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT
 									 cast(null as timestamp) as time,
diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts
index 542e9cc3648..6e05ddc36be 100644
--- a/public/app/features/plugins/ds_edit_ctrl.ts
+++ b/public/app/features/plugins/ds_edit_ctrl.ts
@@ -132,7 +132,7 @@ export class DataSourceEditCtrl {
       this.backendSrv
         .withNoBackendCache(() => {
           return datasource
-            .testDatasource()
+            .testDatasource(this)
             .then(result => {
               this.testing.message = result.message;
               this.testing.status = result.status;
diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts
index 644c9e48b9b..88c928e425a 100644
--- a/public/app/plugins/datasource/postgres/datasource.ts
+++ b/public/app/plugins/datasource/postgres/datasource.ts
@@ -123,27 +123,27 @@ export class PostgresDatasource {
       .then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
   }
 
-  testDatasource() {
-    return this.backendSrv
-      .datasourceRequest({
-        url: '/api/tsdb/query',
-        method: 'POST',
-        data: {
-          from: '5m',
-          to: 'now',
-          queries: [
-            {
-              refId: 'A',
-              intervalMs: 1,
-              maxDataPoints: 1,
-              datasourceId: this.id,
-              rawSql: 'SELECT 1',
-              format: 'table',
-            },
-          ],
-        },
-      })
+  testDatasource(control) {
+    return this.metricFindQuery('SELECT 1', {})
       .then(res => {
+        if (control.current.jsonData.timescaledb === 'auto') {
+          return this.metricFindQuery("SELECT 1 FROM pg_extension WHERE extname='timescaledb'", {})
+            .then(res => {
+              if (res.length === 1) {
+                control.current.jsonData.timescaledb = 'enabled';
+                return this.backendSrv.put('/api/datasources/' + this.id, control.current).then(settings => {
+                  control.current = settings.datasource;
+                  control.updateFrontendSettings();
+                  return { status: 'success', message: 'Database Connection OK, TimescaleDB found' };
+                });
+              }
+              throw new Error('timescaledb not found');
+            })
+            .catch(err => {
+              // query errored out or empty so timescaledb is not available
+              return { status: 'success', message: 'Database Connection OK' };
+            });
+        }
         return { status: 'success', message: 'Database Connection OK' };
       })
       .catch(err => {
diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html
index 77f0dcfa4a5..07568fdc459 100644
--- a/public/app/plugins/datasource/postgres/partials/config.html
+++ b/public/app/plugins/datasource/postgres/partials/config.html
@@ -38,6 +38,20 @@
 	</div>
 </div>
 
+<h3 class="page-heading">PostgreSQL details</h3>
+
+<div class="gf-form-group">
+	<div class="gf-form">
+		<label class="gf-form-label width-7">TimescaleDB</label>
+		<div class="gf-form-select-wrapper max-width-8 gf-form-select-wrapper--has-help-icon">
+			<select class="gf-form-input" ng-model="ctrl.current.jsonData.timescaledb" ng-options="mode for mode in ['auto', 'enabled', 'disabled']" ng-init="ctrl.current.jsonData.timescaledb=ctrl.current.jsonData.timescaledb || 'auto'"></select>
+			<info-popover mode="right-absolute">
+				This option determines whether TimescaleDB features will be used.
+			</info-popover>
+		</div>
+	</div>
+</div>
+
 <div class="gf-form-group">
 	<div class="grafana-info-box">
 		<h5>User Permission</h5>

From c3aad100472063957ecf869115cde521c7d5ccf9 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Thu, 9 Aug 2018 09:19:16 +0200
Subject: [PATCH 267/380] change timescaledb to checkbox instead of select

---
 pkg/tsdb/postgres/macros.go                   |  2 +-
 pkg/tsdb/postgres/macros_test.go              |  2 +-
 .../plugins/datasource/postgres/datasource.ts | 20 +------------------
 .../datasource/postgres/partials/config.html  |  8 +-------
 4 files changed, 4 insertions(+), 28 deletions(-)

diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 4f1d3f72558..69aa04f45f5 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -131,7 +131,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			}
 		}
 
-		if m.query.DataSource.JsonData.Get("timescaledb").MustString("auto") == "enabled" {
+		if m.query.DataSource.JsonData.Get("timescaledb").MustBool() {
 			return fmt.Sprintf("time_bucket('%vs',%s) AS time", interval.Seconds(), args[0]), nil
 		} else {
 			return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index 6c4ba8305b1..8b2fd7a32f8 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -17,7 +17,7 @@ func TestMacroEngine(t *testing.T) {
 		engine := newPostgresMacroEngine()
 		query := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}}
 		queryTS := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}}
-		queryTS.DataSource.JsonData.Set("timescaledb", "enabled")
+		queryTS.DataSource.JsonData.Set("timescaledb", true)
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
 			from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts
index 88c928e425a..3d48dce45b2 100644
--- a/public/app/plugins/datasource/postgres/datasource.ts
+++ b/public/app/plugins/datasource/postgres/datasource.ts
@@ -123,27 +123,9 @@ export class PostgresDatasource {
       .then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
   }
 
-  testDatasource(control) {
+  testDatasource() {
     return this.metricFindQuery('SELECT 1', {})
       .then(res => {
-        if (control.current.jsonData.timescaledb === 'auto') {
-          return this.metricFindQuery("SELECT 1 FROM pg_extension WHERE extname='timescaledb'", {})
-            .then(res => {
-              if (res.length === 1) {
-                control.current.jsonData.timescaledb = 'enabled';
-                return this.backendSrv.put('/api/datasources/' + this.id, control.current).then(settings => {
-                  control.current = settings.datasource;
-                  control.updateFrontendSettings();
-                  return { status: 'success', message: 'Database Connection OK, TimescaleDB found' };
-                });
-              }
-              throw new Error('timescaledb not found');
-            })
-            .catch(err => {
-              // query errored out or empty so timescaledb is not available
-              return { status: 'success', message: 'Database Connection OK' };
-            });
-        }
         return { status: 'success', message: 'Database Connection OK' };
       })
       .catch(err => {
diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html
index 07568fdc459..14b0b03ddb5 100644
--- a/public/app/plugins/datasource/postgres/partials/config.html
+++ b/public/app/plugins/datasource/postgres/partials/config.html
@@ -42,13 +42,7 @@
 
 <div class="gf-form-group">
 	<div class="gf-form">
-		<label class="gf-form-label width-7">TimescaleDB</label>
-		<div class="gf-form-select-wrapper max-width-8 gf-form-select-wrapper--has-help-icon">
-			<select class="gf-form-input" ng-model="ctrl.current.jsonData.timescaledb" ng-options="mode for mode in ['auto', 'enabled', 'disabled']" ng-init="ctrl.current.jsonData.timescaledb=ctrl.current.jsonData.timescaledb || 'auto'"></select>
-			<info-popover mode="right-absolute">
-				This option determines whether TimescaleDB features will be used.
-			</info-popover>
-		</div>
+		<gf-form-switch class="gf-form" label="TimescaleDB" tooltip="Use TimescaleDB features in Grafana" label-class="width-9" checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch>
 	</div>
 </div>
 

From acd1acba2d426270ddb54a6e9b233562ec5f1ebd Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Thu, 9 Aug 2018 09:22:02 +0200
Subject: [PATCH 268/380] revert passing ctrl to testDatasource

---
 public/app/features/plugins/ds_edit_ctrl.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts
index 6e05ddc36be..542e9cc3648 100644
--- a/public/app/features/plugins/ds_edit_ctrl.ts
+++ b/public/app/features/plugins/ds_edit_ctrl.ts
@@ -132,7 +132,7 @@ export class DataSourceEditCtrl {
       this.backendSrv
         .withNoBackendCache(() => {
           return datasource
-            .testDatasource(this)
+            .testDatasource()
             .then(result => {
               this.testing.message = result.message;
               this.testing.status = result.status;

From d2984f3b0f578423a56444516682f475842fa6e7 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Thu, 9 Aug 2018 10:14:14 +0200
Subject: [PATCH 269/380] fix rebase error

---
 pkg/tsdb/postgres/macros.go        | 4 ++--
 pkg/tsdb/postgres/macros_test.go   | 4 ++--
 pkg/tsdb/postgres/postgres_test.go | 1 +
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 69aa04f45f5..d9f97e9262c 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -132,9 +132,9 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		}
 
 		if m.query.DataSource.JsonData.Get("timescaledb").MustBool() {
-			return fmt.Sprintf("time_bucket('%vs',%s) AS time", interval.Seconds(), args[0]), nil
+			return fmt.Sprintf("time_bucket('%vs',%s)", interval.Seconds(), args[0]), nil
 		} else {
-			return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
+			return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
 		}
 	case "__timeGroupAlias":
 		tg, err := m.evaluateMacro("__timeGroup", args)
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index 8b2fd7a32f8..449331224c2 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -92,7 +92,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column) AS time")
+				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)")
 			})
 
 			Convey("interpolate __timeGroup function with spaces between args and TimescaleDB enabled", func() {
@@ -100,7 +100,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column) AS time")
+				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)")
 			})
 
 			Convey("interpolate __timeTo function", func() {
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index 27888b318a9..87b7f916ca9 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -311,6 +311,7 @@ func TestPostgres(t *testing.T) {
 			query := &tsdb.TsdbQuery{
 				Queries: []*tsdb.Query{
 					{
+						DataSource: &models.DataSource{JsonData: simplejson.New()},
 						Model: simplejson.NewFromAny(map[string]interface{}{
 							"rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 							"format": "time_series",

From 9d66eeb10caf08031d935a23ff7f15ad49a12188 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Thu, 9 Aug 2018 10:21:54 +0200
Subject: [PATCH 270/380] Fix padding for metrics chooser in explore

---
 public/vendor/css/rc-cascader.scss | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/public/vendor/css/rc-cascader.scss b/public/vendor/css/rc-cascader.scss
index 5cfaaf4961a..f6e55c62d23 100644
--- a/public/vendor/css/rc-cascader.scss
+++ b/public/vendor/css/rc-cascader.scss
@@ -16,7 +16,7 @@
 }
 .rc-cascader-menus.slide-up-enter,
 .rc-cascader-menus.slide-up-appear {
-  animation-duration: .3s;
+  animation-duration: 0.3s;
   animation-fill-mode: both;
   transform-origin: 0 0;
   opacity: 0;
@@ -24,7 +24,7 @@
   animation-play-state: paused;
 }
 .rc-cascader-menus.slide-up-leave {
-  animation-duration: .3s;
+  animation-duration: 0.3s;
   animation-fill-mode: both;
   transform-origin: 0 0;
   opacity: 1;
@@ -66,7 +66,7 @@
 .rc-cascader-menu-item {
   height: 32px;
   line-height: 32px;
-  padding: 0 16px;
+  padding: 0 2.5em 0 16px;
   cursor: pointer;
   white-space: nowrap;
   overflow: hidden;

From 1c63f7a61ff884db153a959b6e1666ab94366562 Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Thu, 9 Aug 2018 10:51:04 +0200
Subject: [PATCH 271/380] Update NOTICE.md

---
 NOTICE.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/NOTICE.md b/NOTICE.md
index ca148971b62..899b2a3c3f9 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1,5 +1,5 @@
 
-Copyright 2014-2017 Grafana Labs
+Copyright 2014-2018 Grafana Labs
 
 This software is based on Kibana: 
 Copyright 2012-2013 Elasticsearch BV

From f339b3502a7e54fedc58601a61185a539d9c2b3b Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Thu, 9 Aug 2018 12:56:55 +0200
Subject: [PATCH 272/380] replaced confirm delete modal with deleteButton
 component in teams members list

---
 public/app/containers/Teams/TeamMembers.tsx | 18 ++++--------------
 1 file changed, 4 insertions(+), 14 deletions(-)

diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/containers/Teams/TeamMembers.tsx
index 0d0762469a0..88933e00ab1 100644
--- a/public/app/containers/Teams/TeamMembers.tsx
+++ b/public/app/containers/Teams/TeamMembers.tsx
@@ -2,9 +2,9 @@ import React from 'react';
 import { hot } from 'react-hot-loader';
 import { observer } from 'mobx-react';
 import { ITeam, ITeamMember } from 'app/stores/TeamsStore/TeamsStore';
-import appEvents from 'app/core/app_events';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
+import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
 
 interface Props {
   team: ITeam;
@@ -31,15 +31,7 @@ export class TeamMembers extends React.Component<Props, State> {
   };
 
   removeMember(member: ITeamMember) {
-    appEvents.emit('confirm-modal', {
-      title: 'Remove Member',
-      text: 'Are you sure you want to remove ' + member.login + ' from this group?',
-      yesText: 'Remove',
-      icon: 'fa-warning',
-      onConfirm: () => {
-        this.removeMemberConfirmed(member);
-      },
-    });
+    this.props.team.removeMember(member);
   }
 
   removeMemberConfirmed(member: ITeamMember) {
@@ -54,10 +46,8 @@ export class TeamMembers extends React.Component<Props, State> {
         </td>
         <td>{member.login}</td>
         <td>{member.email}</td>
-        <td style={{ width: '1%' }}>
-          <a onClick={() => this.removeMember(member)} className="btn btn-danger btn-mini">
-            <i className="fa fa-remove" />
-          </a>
+        <td className="text-right">
+          <DeleteButton onConfirmDelete={() => this.removeMember(member)} />
         </td>
       </tr>
     );

From 1bb3cf1c3116df8992e293a8f7a27d2c1d9d20e0 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 9 Aug 2018 15:04:56 +0200
Subject: [PATCH 273/380] keep legend scroll position when series are toggled
 (#12845)

---
 public/app/plugins/panel/graph/legend.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts
index af61db396ba..f5c35ad98bf 100644
--- a/public/app/plugins/panel/graph/legend.ts
+++ b/public/app/plugins/panel/graph/legend.ts
@@ -70,9 +70,9 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
         var el = $(e.currentTarget);
         var index = getSeriesIndexForElement(el);
         var seriesInfo = seriesList[index];
-        var scrollPosition = $(elem.children('tbody')).scrollTop();
+        const scrollPosition = legendScrollbar.scroller.scrollTop;
         ctrl.toggleSeries(seriesInfo, e);
-        $(elem.children('tbody')).scrollTop(scrollPosition);
+        legendScrollbar.scroller.scrollTop = scrollPosition;
       }
 
       function sortLegend(e) {

From a4a33d80dbe1ee0dfe4a3a53a434c90919842e76 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Thu, 9 Aug 2018 17:30:46 +0200
Subject: [PATCH 274/380] mention time_bucket in timescaledb tooltip

---
 public/app/plugins/datasource/postgres/partials/config.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/plugins/datasource/postgres/partials/config.html b/public/app/plugins/datasource/postgres/partials/config.html
index 14b0b03ddb5..a1783c09dc4 100644
--- a/public/app/plugins/datasource/postgres/partials/config.html
+++ b/public/app/plugins/datasource/postgres/partials/config.html
@@ -42,7 +42,7 @@
 
 <div class="gf-form-group">
 	<div class="gf-form">
-		<gf-form-switch class="gf-form" label="TimescaleDB" tooltip="Use TimescaleDB features in Grafana" label-class="width-9" checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch>
+		<gf-form-switch class="gf-form" label="TimescaleDB" tooltip="Use TimescaleDB features (e.g., time_bucket) in Grafana" label-class="width-9" checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch>
 	</div>
 </div>
 

From 9188f7423c6340c4898792b0fba594729869d19f Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 6 Aug 2018 09:06:29 +0200
Subject: [PATCH 275/380] Begin conversion

---
 .../panel/heatmap/specs/renderer.jest.ts      | 319 ++++++++++++++++++
 1 file changed, 319 insertions(+)
 create mode 100644 public/app/plugins/panel/heatmap/specs/renderer.jest.ts

diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
new file mode 100644
index 00000000000..4e0e8d1b6a9
--- /dev/null
+++ b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
@@ -0,0 +1,319 @@
+// import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
+
+import '../module';
+import angular from 'angular';
+import $ from 'jquery';
+// import helpers from 'test/specs/helpers';
+import TimeSeries from 'app/core/time_series2';
+import moment from 'moment';
+import { Emitter } from 'app/core/core';
+import rendering from '../rendering';
+import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter';
+
+describe('grafanaHeatmap', function() {
+  //   beforeEach(angularMocks.module('grafana.core'));
+
+  function heatmapScenario(desc, func, elementWidth = 500) {
+    describe(desc, function() {
+      var ctx: any = {};
+
+      ctx.setup = function(setupFunc) {
+        // beforeEach(
+        //   angularMocks.module(function($provide) {
+        //     $provide.value('timeSrv', new helpers.TimeSrvStub());
+        //   })
+        // );
+
+        beforeEach(() => {
+          //   angularMocks.inject(function($rootScope, $compile) {
+          var ctrl: any = {
+            colorSchemes: [
+              {
+                name: 'Oranges',
+                value: 'interpolateOranges',
+                invert: 'dark',
+              },
+              { name: 'Reds', value: 'interpolateReds', invert: 'dark' },
+            ],
+            //   events: new Emitter(),
+            height: 200,
+            panel: {
+              heatmap: {},
+              cards: {
+                cardPadding: null,
+                cardRound: null,
+              },
+              color: {
+                mode: 'spectrum',
+                cardColor: '#b4ff00',
+                colorScale: 'linear',
+                exponent: 0.5,
+                colorScheme: 'interpolateOranges',
+                fillBackground: false,
+              },
+              legend: {
+                show: false,
+              },
+              xBucketSize: 1000,
+              xBucketNumber: null,
+              yBucketSize: 1,
+              yBucketNumber: null,
+              xAxis: {
+                show: true,
+              },
+              yAxis: {
+                show: true,
+                format: 'short',
+                decimals: null,
+                logBase: 1,
+                splitFactor: null,
+                min: null,
+                max: null,
+                removeZeroValues: false,
+              },
+              tooltip: {
+                show: true,
+                seriesStat: false,
+                showHistogram: false,
+              },
+              highlightCards: true,
+            },
+            renderingCompleted: jest.fn(),
+            hiddenSeries: {},
+            dashboard: {
+              getTimezone: () => 'utc',
+            },
+            range: {
+              from: moment.utc('01 Mar 2017 10:00:00', 'DD MMM YYYY HH:mm:ss'),
+              to: moment.utc('01 Mar 2017 11:00:00', 'DD MMM YYYY HH:mm:ss'),
+            },
+          };
+
+          var scope = $rootScope.$new();
+          scope.ctrl = ctrl;
+
+          ctx.series = [];
+          ctx.series.push(
+            new TimeSeries({
+              datapoints: [[1, 1422774000000], [2, 1422774060000]],
+              alias: 'series1',
+            })
+          );
+          ctx.series.push(
+            new TimeSeries({
+              datapoints: [[2, 1422774000000], [3, 1422774060000]],
+              alias: 'series2',
+            })
+          );
+
+          ctx.data = {
+            heatmapStats: {
+              min: 1,
+              max: 3,
+              minLog: 1,
+            },
+            xBucketSize: ctrl.panel.xBucketSize,
+            yBucketSize: ctrl.panel.yBucketSize,
+          };
+
+          setupFunc(ctrl, ctx);
+
+          let logBase = ctrl.panel.yAxis.logBase;
+          let bucketsData;
+          if (ctrl.panel.dataFormat === 'tsbuckets') {
+            bucketsData = histogramToHeatmap(ctx.series);
+          } else {
+            bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
+          }
+          ctx.data.buckets = bucketsData;
+
+          let { cards, cardStats } = convertToCards(bucketsData);
+          ctx.data.cards = cards;
+          ctx.data.cardStats = cardStats;
+
+          let elemHtml = `
+          <div class="heatmap-wrapper">
+            <div class="heatmap-canvas-wrapper">
+              <div class="heatmap-panel" style='width:${elementWidth}px'></div>
+            </div>
+          </div>`;
+
+          var element = $.parseHTML(elemHtml);
+          // $compile(element)(scope);
+          // scope.$digest();
+
+          ctrl.data = ctx.data;
+          ctx.element = element;
+          rendering(scope, $(element), [], ctrl);
+          ctrl.events.emit('render');
+        });
+      };
+
+      func(ctx);
+    });
+  }
+
+  heatmapScenario('default options', function(ctx) {
+    ctx.setup(function(ctrl) {
+      ctrl.panel.yAxis.logBase = 1;
+    });
+
+    it('should draw correct Y axis', function() {
+      var yTicks = getTicks(ctx.element, '.axis-y');
+      expect(yTicks).toEqual(['1', '2', '3']);
+    });
+
+    it('should draw correct X axis', function() {
+      var xTicks = getTicks(ctx.element, '.axis-x');
+      let expectedTicks = [
+        formatTime('01 Mar 2017 10:00:00'),
+        formatTime('01 Mar 2017 10:15:00'),
+        formatTime('01 Mar 2017 10:30:00'),
+        formatTime('01 Mar 2017 10:45:00'),
+        formatTime('01 Mar 2017 11:00:00'),
+      ];
+      expect(xTicks).toEqual(expectedTicks);
+    });
+  });
+
+  heatmapScenario('when logBase is 2', function(ctx) {
+    ctx.setup(function(ctrl) {
+      ctrl.panel.yAxis.logBase = 2;
+    });
+
+    it('should draw correct Y axis', function() {
+      var yTicks = getTicks(ctx.element, '.axis-y');
+      expect(yTicks).toEqual(['1', '2', '4']);
+    });
+  });
+
+  heatmapScenario('when logBase is 10', function(ctx) {
+    ctx.setup(function(ctrl, ctx) {
+      ctrl.panel.yAxis.logBase = 10;
+
+      ctx.series.push(
+        new TimeSeries({
+          datapoints: [[10, 1422774000000], [20, 1422774060000]],
+          alias: 'series3',
+        })
+      );
+      ctx.data.heatmapStats.max = 20;
+    });
+
+    it('should draw correct Y axis', function() {
+      var yTicks = getTicks(ctx.element, '.axis-y');
+      expect(yTicks).toEqual(['1', '10', '100']);
+    });
+  });
+
+  heatmapScenario('when logBase is 32', function(ctx) {
+    ctx.setup(function(ctrl) {
+      ctrl.panel.yAxis.logBase = 32;
+
+      ctx.series.push(
+        new TimeSeries({
+          datapoints: [[10, 1422774000000], [100, 1422774060000]],
+          alias: 'series3',
+        })
+      );
+      ctx.data.heatmapStats.max = 100;
+    });
+
+    it('should draw correct Y axis', function() {
+      var yTicks = getTicks(ctx.element, '.axis-y');
+      expect(yTicks).toEqual(['1', '32', '1.0 K']);
+    });
+  });
+
+  heatmapScenario('when logBase is 1024', function(ctx) {
+    ctx.setup(function(ctrl) {
+      ctrl.panel.yAxis.logBase = 1024;
+
+      ctx.series.push(
+        new TimeSeries({
+          datapoints: [[2000, 1422774000000], [300000, 1422774060000]],
+          alias: 'series3',
+        })
+      );
+      ctx.data.heatmapStats.max = 300000;
+    });
+
+    it('should draw correct Y axis', function() {
+      var yTicks = getTicks(ctx.element, '.axis-y');
+      expect(yTicks).toEqual(['1', '1 K', '1.0 Mil']);
+    });
+  });
+
+  heatmapScenario('when Y axis format set to "none"', function(ctx) {
+    ctx.setup(function(ctrl) {
+      ctrl.panel.yAxis.logBase = 1;
+      ctrl.panel.yAxis.format = 'none';
+      ctx.data.heatmapStats.max = 10000;
+    });
+
+    it('should draw correct Y axis', function() {
+      var yTicks = getTicks(ctx.element, '.axis-y');
+      expect(yTicks).toEqual(['0', '2000', '4000', '6000', '8000', '10000', '12000']);
+    });
+  });
+
+  heatmapScenario('when Y axis format set to "second"', function(ctx) {
+    ctx.setup(function(ctrl) {
+      ctrl.panel.yAxis.logBase = 1;
+      ctrl.panel.yAxis.format = 's';
+      ctx.data.heatmapStats.max = 3600;
+    });
+
+    it('should draw correct Y axis', function() {
+      var yTicks = getTicks(ctx.element, '.axis-y');
+      expect(yTicks).toEqual(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']);
+    });
+  });
+
+  heatmapScenario('when data format is Time series buckets', function(ctx) {
+    ctx.setup(function(ctrl, ctx) {
+      ctrl.panel.dataFormat = 'tsbuckets';
+
+      const series = [
+        {
+          alias: '1',
+          datapoints: [[1000, 1422774000000], [200000, 1422774060000]],
+        },
+        {
+          alias: '2',
+          datapoints: [[3000, 1422774000000], [400000, 1422774060000]],
+        },
+        {
+          alias: '3',
+          datapoints: [[2000, 1422774000000], [300000, 1422774060000]],
+        },
+      ];
+      ctx.series = series.map(s => new TimeSeries(s));
+
+      ctx.data.tsBuckets = series.map(s => s.alias).concat('');
+      ctx.data.yBucketSize = 1;
+      let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]);
+      ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet);
+    });
+
+    it('should draw correct Y axis', function() {
+      var yTicks = getTicks(ctx.element, '.axis-y');
+      expect(yTicks).toEqual(['1', '2', '3', '']);
+    });
+  });
+});
+
+function getTicks(element, axisSelector) {
+  return element
+    .find(axisSelector)
+    .find('text')
+    .map(function() {
+      return this.textContent;
+    })
+    .get();
+}
+
+function formatTime(timeStr) {
+  let format = 'HH:mm';
+  return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format);
+}

From e832f91fb6331ed76ae7fa94e714544c0be516ec Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 10 Aug 2018 13:37:15 +0200
Subject: [PATCH 276/380] Fix initial state in split explore

- remove `edited` from query state to reset queries
- clear more properties in state
---
 public/app/containers/Explore/Explore.tsx | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index 9620ac4f91b..d161e7689cf 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -207,6 +207,7 @@ export class Explore extends React.Component<any, IExploreState> {
       datasourceError: null,
       datasourceLoading: true,
       graphResult: null,
+      latency: 0,
       logsResult: null,
       queryErrors: [],
       queryHints: [],
@@ -254,7 +255,10 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState({
       graphResult: null,
       logsResult: null,
+      latency: 0,
       queries: ensureQueries(),
+      queryErrors: [],
+      queryHints: [],
       tableResult: null,
     });
   };
@@ -276,8 +280,10 @@ export class Explore extends React.Component<any, IExploreState> {
 
   onClickSplit = () => {
     const { onChangeSplit } = this.props;
+    const state = { ...this.state };
+    state.queries = state.queries.map(({ edited, ...rest }) => rest);
     if (onChangeSplit) {
-      onChangeSplit(true, this.state);
+      onChangeSplit(true, state);
     }
   };
 

From 1f88bfd2bcb489823934267b2bcdc16681f996ee Mon Sep 17 00:00:00 2001
From: Daniel Lee <dan.limerick@gmail.com>
Date: Fri, 10 Aug 2018 14:02:51 +0200
Subject: [PATCH 277/380] Add note for #12843

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4983dbafdcd..198b28ca392 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,7 @@
 * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
 * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
 * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
+* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud)
 
 ### Breaking changes
 

From a0fbe3c296efb2082ffb9d3fd3481d6fd1fc6a41 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 10 Aug 2018 14:45:09 +0200
Subject: [PATCH 278/380] Explore: Filter out existing labels in label
 suggestions

- a valid selector returns all possible labels from the series API
- we only want to suggest the label keys that are not part of the
  selector yet
---
 .../Explore/PromQueryField.jest.tsx           | 19 ++++++
 .../app/containers/Explore/PromQueryField.tsx | 16 +++--
 .../Explore/utils/prometheus.jest.ts          | 62 ++++++++++++++-----
 .../containers/Explore/utils/prometheus.ts    | 17 ++---
 4 files changed, 85 insertions(+), 29 deletions(-)

diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.jest.tsx
index 350a529c89e..c82a1cd448f 100644
--- a/public/app/containers/Explore/PromQueryField.jest.tsx
+++ b/public/app/containers/Explore/PromQueryField.jest.tsx
@@ -94,6 +94,25 @@ describe('PromQueryField typeahead handling', () => {
       expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
     });
 
+    it('returns label suggestions on label context but leaves out labels that already exist', () => {
+      const instance = shallow(
+        <PromQueryField {...defaultProps} labelKeys={{ '{job="foo"}': ['bar', 'job'] }} />
+      ).instance() as PromQueryField;
+      const value = Plain.deserialize('{job="foo",}');
+      const range = value.selection.merge({
+        anchorOffset: 11,
+      });
+      const valueWithSelection = value.change().select(range).value;
+      const result = instance.getTypeahead({
+        text: '',
+        prefix: '',
+        wrapperClasses: ['context-labels'],
+        value: valueWithSelection,
+      });
+      expect(result.context).toBe('context-labels');
+      expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
+    });
+
     it('returns a refresher on label context and unavailable metric', () => {
       const instance = shallow(
         <PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />
diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx
index 1b3ff33971d..0991f08429a 100644
--- a/public/app/containers/Explore/PromQueryField.tsx
+++ b/public/app/containers/Explore/PromQueryField.tsx
@@ -10,7 +10,7 @@ import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
 import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
 import BracesPlugin from './slate-plugins/braces';
 import RunnerPlugin from './slate-plugins/runner';
-import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus';
+import { processLabels, RATE_RANGES, cleanText, parseSelector } from './utils/prometheus';
 
 import TypeaheadField, {
   Suggestion,
@@ -328,7 +328,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
     const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
     // foo{bar="1"}
     const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
-    const selector = getCleanSelector(selectorString, selectorString.length - 2);
+    const selector = parseSelector(selectorString, selectorString.length - 2).selector;
 
     const labelKeys = this.state.labelKeys[selector];
     if (labelKeys) {
@@ -353,12 +353,15 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
 
     // Get normalized selector
     let selector;
+    let parsedSelector;
     try {
-      selector = getCleanSelector(line, cursorOffset);
+      parsedSelector = parseSelector(line, cursorOffset);
+      selector = parsedSelector.selector;
     } catch {
       selector = EMPTY_SELECTOR;
     }
     const containsMetric = selector.indexOf('__name__=') > -1;
+    const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
 
     if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
       // Label values
@@ -374,8 +377,11 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
       // Label keys
       const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
       if (labelKeys) {
-        context = 'context-labels';
-        suggestions.push({ label: `Labels`, items: labelKeys.map(wrapLabel) });
+        const possibleKeys = _.difference(labelKeys, existingKeys);
+        if (possibleKeys.length > 0) {
+          context = 'context-labels';
+          suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
+        }
       }
     }
 
diff --git a/public/app/containers/Explore/utils/prometheus.jest.ts b/public/app/containers/Explore/utils/prometheus.jest.ts
index febaecc29b5..d12d28c6bc9 100644
--- a/public/app/containers/Explore/utils/prometheus.jest.ts
+++ b/public/app/containers/Explore/utils/prometheus.jest.ts
@@ -1,33 +1,61 @@
-import { getCleanSelector } from './prometheus';
+import { parseSelector } from './prometheus';
+
+describe('parseSelector()', () => {
+  let parsed;
 
-describe('getCleanSelector()', () => {
   it('returns a clean selector from an empty selector', () => {
-    expect(getCleanSelector('{}', 1)).toBe('{}');
+    parsed = parseSelector('{}', 1);
+    expect(parsed.selector).toBe('{}');
+    expect(parsed.labelKeys).toEqual([]);
   });
+
   it('throws if selector is broken', () => {
-    expect(() => getCleanSelector('{foo')).toThrow();
+    expect(() => parseSelector('{foo')).toThrow();
   });
+
   it('returns the selector sorted by label key', () => {
-    expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}');
-    expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",foo="bar"}');
+    parsed = parseSelector('{foo="bar"}');
+    expect(parsed.selector).toBe('{foo="bar"}');
+    expect(parsed.labelKeys).toEqual(['foo']);
+
+    parsed = parseSelector('{foo="bar",baz="xx"}');
+    expect(parsed.selector).toBe('{baz="xx",foo="bar"}');
   });
+
   it('returns a clean selector from an incomplete one', () => {
-    expect(getCleanSelector('{foo}')).toBe('{}');
-    expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}');
-    expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}');
+    parsed = parseSelector('{foo}');
+    expect(parsed.selector).toBe('{}');
+
+    parsed = parseSelector('{foo="bar",baz}');
+    expect(parsed.selector).toBe('{foo="bar"}');
+
+    parsed = parseSelector('{foo="bar",baz="}');
+    expect(parsed.selector).toBe('{foo="bar"}');
   });
+
   it('throws if not inside a selector', () => {
-    expect(() => getCleanSelector('foo{}', 0)).toThrow();
-    expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow();
+    expect(() => parseSelector('foo{}', 0)).toThrow();
+    expect(() => parseSelector('foo{} + bar{}', 5)).toThrow();
   });
+
   it('returns the selector nearest to the cursor offset', () => {
-    expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
-    expect(getCleanSelector('{foo="bar"} + {foo="bar"}', 1)).toBe('{foo="bar"}');
-    expect(getCleanSelector('{foo="bar"} + {baz="xx"}', 1)).toBe('{foo="bar"}');
-    expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}');
+    expect(() => parseSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
+
+    parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1);
+    expect(parsed.selector).toBe('{foo="bar"}');
+
+    parsed = parseSelector('{foo="bar"} + {baz="xx"}', 1);
+    expect(parsed.selector).toBe('{foo="bar"}');
+
+    parsed = parseSelector('{baz="xx"} + {foo="bar"}', 16);
+    expect(parsed.selector).toBe('{foo="bar"}');
   });
+
   it('returns a selector with metric if metric is given', () => {
-    expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}');
-    expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}');
+    parsed = parseSelector('bar{foo}', 4);
+    expect(parsed.selector).toBe('{__name__="bar"}');
+
+    parsed = parseSelector('baz{foo="bar"}', 12);
+    expect(parsed.selector).toBe('{__name__="baz",foo="bar"}');
   });
 });
diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts
index ab77271076d..f5ccb848f2f 100644
--- a/public/app/containers/Explore/utils/prometheus.ts
+++ b/public/app/containers/Explore/utils/prometheus.ts
@@ -29,11 +29,14 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
 // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
 const selectorRegexp = /\{[^}]*?\}/;
 const labelRegexp = /\b\w+="[^"\n]*?"/g;
-export function getCleanSelector(query: string, cursorOffset = 1): string {
+export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
   if (!query.match(selectorRegexp)) {
     // Special matcher for metrics
     if (query.match(/^\w+$/)) {
-      return `{__name__="${query}"}`;
+      return {
+        selector: `{__name__="${query}"}`,
+        labelKeys: ['__name__'],
+      };
     }
     throw new Error('Query must contain a selector: ' + query);
   }
@@ -79,10 +82,10 @@ export function getCleanSelector(query: string, cursorOffset = 1): string {
   }
 
   // Build sorted selector
-  const cleanSelector = Object.keys(labels)
-    .sort()
-    .map(key => `${key}=${labels[key]}`)
-    .join(',');
+  const labelKeys = Object.keys(labels).sort();
+  const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(',');
 
-  return ['{', cleanSelector, '}'].join('');
+  const selectorString = ['{', cleanSelector, '}'].join('');
+
+  return { labelKeys, selector: selectorString };
 }

From 0f5945c5578b3a4e2d469a4d2fb0bf3efde2db09 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Fri, 10 Aug 2018 15:29:21 +0200
Subject: [PATCH 279/380] Explore: still show rate hint if query is complex

- action hint currently only works for very simple queries
- show a hint w/o action otherwise
---
 .../datasource/prometheus/datasource.ts       | 24 ++++++++++++-------
 .../prometheus/specs/datasource.jest.ts       | 24 +++++++++++++++++++
 2 files changed, 39 insertions(+), 9 deletions(-)

diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index ef440ab515d..208a7b6a2f0 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -110,10 +110,9 @@ export function determineQueryHints(series: any[], datasource?: any): any[] {
 
     // Check for monotony
     const datapoints: [number, number][] = s.datapoints;
-    const simpleMetric = query.trim().match(/^\w+$/);
-    if (simpleMetric && datapoints.length > 1) {
+    if (datapoints.length > 1) {
       let increasing = false;
-      const monotonic = datapoints.every((dp, index) => {
+      const monotonic = datapoints.filter(dp => dp[0] !== null).every((dp, index) => {
         if (index === 0) {
           return true;
         }
@@ -122,18 +121,25 @@ export function determineQueryHints(series: any[], datasource?: any): any[] {
         return dp[0] >= datapoints[index - 1][0];
       });
       if (increasing && monotonic) {
-        const label = 'Time series is monotonously increasing.';
-        return {
-          label,
-          index,
-          fix: {
+        const simpleMetric = query.trim().match(/^\w+$/);
+        let label = 'Time series is monotonously increasing.';
+        let fix;
+        if (simpleMetric) {
+          fix = {
             label: 'Fix by adding rate().',
             action: {
               type: 'ADD_RATE',
               query,
               index,
             },
-          },
+          };
+        } else {
+          label = `${label} Try applying a rate() function.`;
+        }
+        return {
+          label,
+          index,
+          fix,
         };
       }
     }
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index a108909e6e1..fea60658332 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -213,6 +213,30 @@ describe('PrometheusDatasource', () => {
       });
     });
 
+    it('returns a rate hint w/o action for a complex monotonously increasing series', () => {
+      const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'sum(metric)', responseIndex: 0 }];
+      const hints = determineQueryHints(series);
+      expect(hints.length).toBe(1);
+      expect(hints[0].label).toContain('rate()');
+      expect(hints[0].fix).toBeUndefined();
+    });
+
+    it('returns a rate hint for a monotonously increasing series with missing data', () => {
+      const series = [{ datapoints: [[23, 1000], [null, 1001], [24, 1002]], query: 'metric', responseIndex: 0 }];
+      const hints = determineQueryHints(series);
+      expect(hints.length).toBe(1);
+      expect(hints[0]).toMatchObject({
+        label: 'Time series is monotonously increasing.',
+        index: 0,
+        fix: {
+          action: {
+            type: 'ADD_RATE',
+            query: 'metric',
+          },
+        },
+      });
+    });
+
     it('returns a histogram hint for a bucket series', () => {
       const series = [{ datapoints: [[23, 1000]], query: 'metric_bucket', responseIndex: 0 }];
       const hints = determineQueryHints(series);

From 076bfea3628861189a41c6e363d3311bbfe4f49b Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 10 Aug 2018 15:35:47 +0200
Subject: [PATCH 280/380] Rewrite heatmap to class

---
 .../app/plugins/panel/heatmap/heatmap_ctrl.ts |   2 +-
 public/app/plugins/panel/heatmap/rendering.ts | 696 +++++++++---------
 2 files changed, 353 insertions(+), 345 deletions(-)

diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
index 1d35ff2ea84..1749403edf0 100644
--- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
+++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
@@ -358,6 +358,6 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
   }
 
   link(scope, elem, attrs, ctrl) {
-    rendering(scope, elem, attrs, ctrl);
+    let render = new rendering(scope, elem, attrs, ctrl);
   }
 }
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index 54d17146532..d54eb5750cd 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/public/app/plugins/panel/heatmap/rendering.ts
@@ -19,56 +19,91 @@ let MIN_CARD_SIZE = 1,
   Y_AXIS_TICK_PADDING = 5,
   MIN_SELECTION_WIDTH = 2;
 
-export default function link(scope, elem, attrs, ctrl) {
-  let data, timeRange, panel, heatmap;
+export default class Link {
+  width: number;
+  height: number;
+  yScale: any;
+  xScale: any;
+  chartWidth: number;
+  chartHeight: number;
+  chartTop: number;
+  chartBottom: number;
+  yAxisWidth: number;
+  xAxisHeight: number;
+  cardPadding: number;
+  cardRound: number;
+  cardWidth: number;
+  cardHeight: number;
+  colorScale: any;
+  opacityScale: any;
+  mouseUpHandler: any;
+  data: any;
+  panel: any;
+  $heatmap: any;
+  tooltip: HeatmapTooltip;
+  heatmap: any;
+  timeRange: any;
 
-  // $heatmap is JQuery object, but heatmap is D3
-  let $heatmap = elem.find('.heatmap-panel');
-  let tooltip = new HeatmapTooltip($heatmap, scope);
+  selection: any;
+  padding: any;
+  margin: any;
+  dataRangeWidingFactor: number;
+  constructor(private scope, private elem, attrs, private ctrl) {
+    // $heatmap is JQuery object, but heatmap is D3
+    this.$heatmap = elem.find('.heatmap-panel');
+    this.tooltip = new HeatmapTooltip(this.$heatmap, this.scope);
 
-  let width,
-    height,
-    yScale,
-    xScale,
-    chartWidth,
-    chartHeight,
-    chartTop,
-    chartBottom,
-    yAxisWidth,
-    xAxisHeight,
-    cardPadding,
-    cardRound,
-    cardWidth,
-    cardHeight,
-    colorScale,
-    opacityScale,
-    mouseUpHandler;
+    this.selection = {
+      active: false,
+      x1: -1,
+      x2: -1,
+    };
 
-  let selection = {
-    active: false,
-    x1: -1,
-    x2: -1,
-  };
+    this.padding = { left: 0, right: 0, top: 0, bottom: 0 };
+    this.margin = { left: 25, right: 15, top: 10, bottom: 20 };
+    this.dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;
 
-  let padding = { left: 0, right: 0, top: 0, bottom: 0 },
-    margin = { left: 25, right: 15, top: 10, bottom: 20 },
-    dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;
+    this.ctrl.events.on('render', this.onRender.bind(this));
 
-  ctrl.events.on('render', () => {
-    render();
-    ctrl.renderingCompleted();
-  });
+    this.ctrl.tickValueFormatter = this.tickValueFormatter;
+    /////////////////////////////
+    // Selection and crosshair //
+    /////////////////////////////
 
-  function setElementHeight() {
+    // Shared crosshair and tooltip
+    appEvents.on('graph-hover', this.onGraphHover.bind(this), this.scope);
+
+    appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), this.scope);
+
+    // Register selection listeners
+    this.$heatmap.on('mousedown', this.onMouseDown.bind(this));
+    this.$heatmap.on('mousemove', this.onMouseMove.bind(this));
+    this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this));
+  }
+
+  onGraphHoverClear() {
+    this.clearCrosshair();
+  }
+
+  onGraphHover(event) {
+    this.drawSharedCrosshair(event.pos);
+  }
+
+  onRender() {
+    this.render();
+    this.ctrl.renderingCompleted();
+  }
+
+  setElementHeight() {
     try {
-      var height = ctrl.height || panel.height || ctrl.row.height;
+      var height = this.ctrl.height || this.panel.height || this.ctrl.row.height;
       if (_.isString(height)) {
         height = parseInt(height.replace('px', ''), 10);
       }
 
-      height -= panel.legend.show ? 28 : 11; // bottom padding and space for legend
+      height -= this.panel.legend.show ? 28 : 11; // bottom padding and space for legend
 
-      $heatmap.css('height', height + 'px');
+      this.$heatmap.css('height', height + 'px');
 
       return true;
     } catch (e) {
@@ -77,7 +112,7 @@ export default function link(scope, elem, attrs, ctrl) {
     }
   }
 
-  function getYAxisWidth(elem) {
+  getYAxisWidth(elem) {
     let axis_text = elem.selectAll('.axis-y text').nodes();
     let max_text_width = _.max(
       _.map(axis_text, text => {
@@ -89,7 +124,7 @@ export default function link(scope, elem, attrs, ctrl) {
     return max_text_width;
   }
 
-  function getXAxisHeight(elem) {
+  getXAxisHeight(elem) {
     let axis_line = elem.select('.axis-x line');
     if (!axis_line.empty()) {
       let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2'));
@@ -101,16 +136,16 @@ export default function link(scope, elem, attrs, ctrl) {
     }
   }
 
-  function addXAxis() {
-    scope.xScale = xScale = d3
+  addXAxis() {
+    this.scope.xScale = this.xScale = d3
       .scaleTime()
-      .domain([timeRange.from, timeRange.to])
-      .range([0, chartWidth]);
+      .domain([this.timeRange.from, this.timeRange.to])
+      .range([0, this.chartWidth]);
 
-    let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX;
-    let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, timeRange.from, timeRange.to);
+    let ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
+    let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
     let timeFormat;
-    let dashboardTimeZone = ctrl.dashboard.getTimezone();
+    let dashboardTimeZone = this.ctrl.dashboard.getTimezone();
     if (dashboardTimeZone === 'utc') {
       timeFormat = d3.utcFormat(grafanaTimeFormatter);
     } else {
@@ -118,100 +153,100 @@ export default function link(scope, elem, attrs, ctrl) {
     }
 
     let xAxis = d3
-      .axisBottom(xScale)
+      .axisBottom(this.xScale)
       .ticks(ticks)
       .tickFormat(timeFormat)
       .tickPadding(X_AXIS_TICK_PADDING)
-      .tickSize(chartHeight);
+      .tickSize(this.chartHeight);
 
-    let posY = margin.top;
-    let posX = yAxisWidth;
-    heatmap
+    let posY = this.margin.top;
+    let posX = this.yAxisWidth;
+    this.heatmap
       .append('g')
       .attr('class', 'axis axis-x')
       .attr('transform', 'translate(' + posX + ',' + posY + ')')
       .call(xAxis);
 
     // Remove horizontal line in the top of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-x')
       .select('.domain')
       .remove();
   }
 
-  function addYAxis() {
-    let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX);
-    let tick_interval = ticksUtils.tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks);
-    let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval);
+  addYAxis() {
+    let ticks = Math.ceil(this.chartHeight / DEFAULT_Y_TICK_SIZE_PX);
+    let tick_interval = ticksUtils.tickStep(this.data.heatmapStats.min, this.data.heatmapStats.max, ticks);
+    let { y_min, y_max } = this.wideYAxisRange(this.data.heatmapStats.min, this.data.heatmapStats.max, tick_interval);
 
     // Rewrite min and max if it have been set explicitly
-    y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min;
-    y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max;
+    y_min = this.panel.yAxis.min !== null ? this.panel.yAxis.min : y_min;
+    y_max = this.panel.yAxis.max !== null ? this.panel.yAxis.max : y_max;
 
     // Adjust ticks after Y range widening
     tick_interval = ticksUtils.tickStep(y_min, y_max, ticks);
     ticks = Math.ceil((y_max - y_min) / tick_interval);
 
     let decimalsAuto = ticksUtils.getPrecision(tick_interval);
-    let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
+    let decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
     // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
     let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
     let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
-    ctrl.decimals = decimals;
-    ctrl.scaledDecimals = scaledDecimals;
+    this.ctrl.decimals = decimals;
+    this.ctrl.scaledDecimals = scaledDecimals;
 
     // Set default Y min and max if no data
-    if (_.isEmpty(data.buckets)) {
+    if (_.isEmpty(this.data.buckets)) {
       y_max = 1;
       y_min = -1;
       ticks = 3;
       decimals = 1;
     }
 
-    data.yAxis = {
+    this.data.yAxis = {
       min: y_min,
       max: y_max,
       ticks: ticks,
     };
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLinear()
       .domain([y_min, y_max])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .ticks(ticks)
-      .tickFormat(tickValueFormatter(decimals, scaledDecimals))
-      .tickSizeInner(0 - width)
+      .tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
 
-    heatmap
+    this.heatmap
       .append('g')
       .attr('class', 'axis axis-y')
       .call(yAxis);
 
     // Calculate Y axis width first, then move axis into visible area
-    let posY = margin.top;
-    let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    let posY = this.margin.top;
+    let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.domain')
       .remove();
   }
 
   // Wide Y values range and anjust to bucket size
-  function wideYAxisRange(min, max, tickInterval) {
-    let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
+  wideYAxisRange(min, max, tickInterval) {
+    let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2;
     let y_min, y_max;
 
     if (tickInterval === 0) {
-      y_max = max * dataRangeWidingFactor;
-      y_min = min - min * (dataRangeWidingFactor - 1);
+      y_max = max * this.dataRangeWidingFactor;
+      y_min = min - min * (this.dataRangeWidingFactor - 1);
       tickInterval = (y_max - y_min) / 2;
     } else {
       y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval;
@@ -226,152 +261,153 @@ export default function link(scope, elem, attrs, ctrl) {
     return { y_min, y_max };
   }
 
-  function addLogYAxis() {
-    let log_base = panel.yAxis.logBase;
-    let { y_min, y_max } = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base);
+  addLogYAxis() {
+    let log_base = this.panel.yAxis.logBase;
+    let { y_min, y_max } = this.adjustLogRange(this.data.heatmapStats.minLog, this.data.heatmapStats.max, log_base);
 
-    y_min = panel.yAxis.min && panel.yAxis.min !== '0' ? adjustLogMin(panel.yAxis.min, log_base) : y_min;
-    y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max;
+    y_min =
+      this.panel.yAxis.min && this.panel.yAxis.min !== '0' ? this.adjustLogMin(this.panel.yAxis.min, log_base) : y_min;
+    y_max = this.panel.yAxis.max !== null ? this.adjustLogMax(this.panel.yAxis.max, log_base) : y_max;
 
     // Set default Y min and max if no data
-    if (_.isEmpty(data.buckets)) {
+    if (_.isEmpty(this.data.buckets)) {
       y_max = Math.pow(log_base, 2);
       y_min = 1;
     }
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLog()
-      .base(panel.yAxis.logBase)
+      .base(this.panel.yAxis.logBase)
       .domain([y_min, y_max])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
-    let domain = yScale.domain();
-    let tick_values = logScaleTickValues(domain, log_base);
+    let domain = this.yScale.domain();
+    let tick_values = this.logScaleTickValues(domain, log_base);
 
     let decimalsAuto = ticksUtils.getPrecision(y_min);
-    let decimals = panel.yAxis.decimals || decimalsAuto;
+    let decimals = this.panel.yAxis.decimals || decimalsAuto;
 
     // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
     let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto);
     let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
-    ctrl.decimals = decimals;
-    ctrl.scaledDecimals = scaledDecimals;
+    this.ctrl.decimals = decimals;
+    this.ctrl.scaledDecimals = scaledDecimals;
 
-    data.yAxis = {
+    this.data.yAxis = {
       min: y_min,
       max: y_max,
       ticks: tick_values.length,
     };
 
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .tickValues(tick_values)
-      .tickFormat(tickValueFormatter(decimals, scaledDecimals))
-      .tickSizeInner(0 - width)
+      .tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
 
-    heatmap
+    this.heatmap
       .append('g')
       .attr('class', 'axis axis-y')
       .call(yAxis);
 
     // Calculate Y axis width first, then move axis into visible area
-    let posY = margin.top;
-    let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    let posY = this.margin.top;
+    let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
     // Set first tick as pseudo 0
     if (y_min < 1) {
-      heatmap
+      this.heatmap
         .select('.axis-y')
         .select('.tick text')
         .text('0');
     }
 
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.domain')
       .remove();
   }
 
-  function addYAxisFromBuckets() {
-    const tsBuckets = data.tsBuckets;
+  addYAxisFromBuckets() {
+    const tsBuckets = this.data.tsBuckets;
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLinear()
       .domain([0, tsBuckets.length - 1])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
     const tick_values = _.map(tsBuckets, (b, i) => i);
     const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision));
-    const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
-    ctrl.decimals = decimals;
+    const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
+    this.ctrl.decimals = decimals;
 
     function tickFormatter(valIndex) {
       let valueFormatted = tsBuckets[valIndex];
       if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
         // Try to format numeric tick labels
-        valueFormatted = tickValueFormatter(decimals)(_.toNumber(valueFormatted));
+        valueFormatted = this.tickValueFormatter(decimals)(_.toNumber(valueFormatted));
       }
       return valueFormatted;
     }
 
     const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i));
-    data.tsBucketsFormatted = tsBucketsFormatted;
+    this.data.tsBucketsFormatted = tsBucketsFormatted;
 
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .tickValues(tick_values)
       .tickFormat(tickFormatter)
-      .tickSizeInner(0 - width)
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
 
-    heatmap
+    this.heatmap
       .append('g')
       .attr('class', 'axis axis-y')
       .call(yAxis);
 
     // Calculate Y axis width first, then move axis into visible area
-    const posY = margin.top;
-    const posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    const posY = this.margin.top;
+    const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.domain')
       .remove();
   }
 
   // Adjust data range to log base
-  function adjustLogRange(min, max, logBase) {
+  adjustLogRange(min, max, logBase) {
     let y_min, y_max;
 
-    y_min = data.heatmapStats.minLog;
-    if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) {
+    y_min = this.data.heatmapStats.minLog;
+    if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) {
       y_min = 1;
     } else {
-      y_min = adjustLogMin(data.heatmapStats.minLog, logBase);
+      y_min = this.adjustLogMin(this.data.heatmapStats.minLog, logBase);
     }
 
     // Adjust max Y value to log base
-    y_max = adjustLogMax(data.heatmapStats.max, logBase);
+    y_max = this.adjustLogMax(this.data.heatmapStats.max, logBase);
 
     return { y_min, y_max };
   }
 
-  function adjustLogMax(max, base) {
+  adjustLogMax(max, base) {
     return Math.pow(base, Math.ceil(ticksUtils.logp(max, base)));
   }
 
-  function adjustLogMin(min, base) {
+  adjustLogMin(min, base) {
     return Math.pow(base, Math.floor(ticksUtils.logp(min, base)));
   }
 
-  function logScaleTickValues(domain, base) {
+  logScaleTickValues(domain, base) {
     let domainMin = domain[0];
     let domainMax = domain[1];
     let tickValues = [];
@@ -393,8 +429,8 @@ export default function link(scope, elem, attrs, ctrl) {
     return tickValues;
   }
 
-  function tickValueFormatter(decimals, scaledDecimals = null) {
-    let format = panel.yAxis.format;
+  tickValueFormatter(decimals, scaledDecimals = null) {
+    let format = this.panel.yAxis.format;
     return function(value) {
       try {
         return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value;
@@ -405,181 +441,179 @@ export default function link(scope, elem, attrs, ctrl) {
     };
   }
 
-  ctrl.tickValueFormatter = tickValueFormatter;
-
-  function fixYAxisTickSize() {
-    heatmap
+  fixYAxisTickSize() {
+    this.heatmap
       .select('.axis-y')
       .selectAll('.tick line')
-      .attr('x2', chartWidth);
+      .attr('x2', this.chartWidth);
   }
 
-  function addAxes() {
-    chartHeight = height - margin.top - margin.bottom;
-    chartTop = margin.top;
-    chartBottom = chartTop + chartHeight;
+  addAxes() {
+    this.chartHeight = this.height - this.margin.top - this.margin.bottom;
+    this.chartTop = this.margin.top;
+    this.chartBottom = this.chartTop + this.chartHeight;
 
-    if (panel.dataFormat === 'tsbuckets') {
-      addYAxisFromBuckets();
+    if (this.panel.dataFormat === 'tsbuckets') {
+      this.addYAxisFromBuckets();
     } else {
-      if (panel.yAxis.logBase === 1) {
-        addYAxis();
+      if (this.panel.yAxis.logBase === 1) {
+        this.addYAxis();
       } else {
-        addLogYAxis();
+        this.addLogYAxis();
       }
     }
 
-    yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    chartWidth = width - yAxisWidth - margin.right;
-    fixYAxisTickSize();
+    this.yAxisWidth = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.chartWidth = this.width - this.yAxisWidth - this.margin.right;
+    this.fixYAxisTickSize();
 
-    addXAxis();
-    xAxisHeight = getXAxisHeight(heatmap);
+    this.addXAxis();
+    this.xAxisHeight = this.getXAxisHeight(this.heatmap);
 
-    if (!panel.yAxis.show) {
-      heatmap
+    if (!this.panel.yAxis.show) {
+      this.heatmap
         .select('.axis-y')
         .selectAll('line')
         .style('opacity', 0);
     }
 
-    if (!panel.xAxis.show) {
-      heatmap
+    if (!this.panel.xAxis.show) {
+      this.heatmap
         .select('.axis-x')
         .selectAll('line')
         .style('opacity', 0);
     }
   }
 
-  function addHeatmapCanvas() {
-    let heatmap_elem = $heatmap[0];
+  addHeatmapCanvas() {
+    let heatmap_elem = this.$heatmap[0];
 
-    width = Math.floor($heatmap.width()) - padding.right;
-    height = Math.floor($heatmap.height()) - padding.bottom;
+    this.width = Math.floor(this.$heatmap.width()) - this.padding.right;
+    this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom;
 
-    cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
-    cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
+    this.cardPadding = this.panel.cards.cardPadding !== null ? this.panel.cards.cardPadding : CARD_PADDING;
+    this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND;
 
-    if (heatmap) {
-      heatmap.remove();
+    if (this.heatmap) {
+      this.heatmap.remove();
     }
 
-    heatmap = d3
+    this.heatmap = d3
       .select(heatmap_elem)
       .append('svg')
-      .attr('width', width)
-      .attr('height', height);
+      .attr('width', this.width)
+      .attr('height', this.height);
   }
 
-  function addHeatmap() {
-    addHeatmapCanvas();
-    addAxes();
+  addHeatmap() {
+    this.addHeatmapCanvas();
+    this.addAxes();
 
-    if (panel.yAxis.logBase !== 1 && panel.dataFormat !== 'tsbuckets') {
-      let log_base = panel.yAxis.logBase;
-      let domain = yScale.domain();
-      let tick_values = logScaleTickValues(domain, log_base);
-      data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
+    if (this.panel.yAxis.logBase !== 1 && this.panel.dataFormat !== 'tsbuckets') {
+      let log_base = this.panel.yAxis.logBase;
+      let domain = this.yScale.domain();
+      let tick_values = this.logScaleTickValues(domain, log_base);
+      this.data.buckets = mergeZeroBuckets(this.data.buckets, _.min(tick_values));
     }
 
-    let cardsData = data.cards;
-    let maxValueAuto = data.cardStats.max;
-    let maxValue = panel.color.max || maxValueAuto;
-    let minValue = panel.color.min || 0;
+    let cardsData = this.data.cards;
+    let maxValueAuto = this.data.cardStats.max;
+    let maxValue = this.panel.color.max || maxValueAuto;
+    let minValue = this.panel.color.min || 0;
 
-    let colorScheme = _.find(ctrl.colorSchemes, {
-      value: panel.color.colorScheme,
+    let colorScheme = _.find(this.ctrl.colorSchemes, {
+      value: this.panel.color.colorScheme,
     });
-    colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
-    opacityScale = getOpacityScale(panel.color, maxValue);
-    setCardSize();
+    this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
+    this.opacityScale = getOpacityScale(this.panel.color, maxValue);
+    this.setCardSize();
 
-    let cards = heatmap.selectAll('.heatmap-card').data(cardsData);
+    let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData);
     cards.append('title');
     cards = cards
       .enter()
       .append('rect')
-      .attr('x', getCardX)
-      .attr('width', getCardWidth)
-      .attr('y', getCardY)
-      .attr('height', getCardHeight)
-      .attr('rx', cardRound)
-      .attr('ry', cardRound)
+      .attr('x', this.getCardX)
+      .attr('width', this.getCardWidth)
+      .attr('y', this.getCardY)
+      .attr('height', this.getCardHeight)
+      .attr('rx', this.cardRound)
+      .attr('ry', this.cardRound)
       .attr('class', 'bordered heatmap-card')
-      .style('fill', getCardColor)
-      .style('stroke', getCardColor)
+      .style('fill', this.getCardColor)
+      .style('stroke', this.getCardColor)
       .style('stroke-width', 0)
-      .style('opacity', getCardOpacity);
+      .style('opacity', this.getCardOpacity);
 
-    let $cards = $heatmap.find('.heatmap-card');
+    let $cards = this.$heatmap.find('.heatmap-card');
     $cards
       .on('mouseenter', event => {
-        tooltip.mouseOverBucket = true;
-        highlightCard(event);
+        this.tooltip.mouseOverBucket = true;
+        this.highlightCard(event);
       })
       .on('mouseleave', event => {
-        tooltip.mouseOverBucket = false;
-        resetCardHighLight(event);
+        this.tooltip.mouseOverBucket = false;
+        this.resetCardHighLight(event);
       });
   }
 
-  function highlightCard(event) {
+  highlightCard(event) {
     let color = d3.select(event.target).style('fill');
     let highlightColor = d3.color(color).darker(2);
     let strokeColor = d3.color(color).brighter(4);
     let current_card = d3.select(event.target);
-    tooltip.originalFillColor = color;
+    this.tooltip.originalFillColor = color;
     current_card
       .style('fill', highlightColor.toString())
       .style('stroke', strokeColor.toString())
       .style('stroke-width', 1);
   }
 
-  function resetCardHighLight(event) {
+  resetCardHighLight(event) {
     d3
       .select(event.target)
-      .style('fill', tooltip.originalFillColor)
-      .style('stroke', tooltip.originalFillColor)
+      .style('fill', this.tooltip.originalFillColor)
+      .style('stroke', this.tooltip.originalFillColor)
       .style('stroke-width', 0);
   }
 
-  function setCardSize() {
-    let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0));
-    let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize));
+  setCardSize() {
+    let xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0));
+    let yGridSize = Math.floor(this.yScale(this.yScale.invert(0) - this.data.yBucketSize));
 
-    if (panel.yAxis.logBase !== 1) {
-      let base = panel.yAxis.logBase;
-      let splitFactor = data.yBucketSize || 1;
-      yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor);
+    if (this.panel.yAxis.logBase !== 1) {
+      let base = this.panel.yAxis.logBase;
+      let splitFactor = this.data.yBucketSize || 1;
+      yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor);
     }
 
-    cardWidth = xGridSize - cardPadding * 2;
-    cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
+    this.cardWidth = xGridSize - this.cardPadding * 2;
+    this.cardHeight = yGridSize ? yGridSize - this.cardPadding * 2 : 0;
   }
 
-  function getCardX(d) {
+  getCardX(d) {
     let x;
-    if (xScale(d.x) < 0) {
+    if (this.xScale(d.x) < 0) {
       // Cut card left to prevent overlay
-      x = yAxisWidth + cardPadding;
+      x = this.yAxisWidth + this.cardPadding;
     } else {
-      x = xScale(d.x) + yAxisWidth + cardPadding;
+      x = this.xScale(d.x) + this.yAxisWidth + this.cardPadding;
     }
 
     return x;
   }
 
-  function getCardWidth(d) {
+  getCardWidth(d) {
     let w;
-    if (xScale(d.x) < 0) {
+    if (this.xScale(d.x) < 0) {
       // Cut card left to prevent overlay
-      let cutted_width = xScale(d.x) + cardWidth;
+      let cutted_width = this.xScale(d.x) + this.cardWidth;
       w = cutted_width > 0 ? cutted_width : 0;
-    } else if (xScale(d.x) + cardWidth > chartWidth) {
+    } else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) {
       // Cut card right to prevent overlay
-      w = chartWidth - xScale(d.x) - cardPadding;
+      w = this.chartWidth - this.xScale(d.x) - this.cardPadding;
     } else {
-      w = cardWidth;
+      w = this.cardWidth;
     }
 
     // Card width should be MIN_CARD_SIZE at least
@@ -587,138 +621,117 @@ export default function link(scope, elem, attrs, ctrl) {
     return w;
   }
 
-  function getCardY(d) {
-    let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
-    if (panel.yAxis.logBase !== 1 && d.y === 0) {
-      y = chartBottom - cardHeight - cardPadding;
+  getCardY(d) {
+    let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding;
+    if (this.panel.yAxis.logBase !== 1 && d.y === 0) {
+      y = this.chartBottom - this.cardHeight - this.cardPadding;
     } else {
-      if (y < chartTop) {
-        y = chartTop;
+      if (y < this.chartTop) {
+        y = this.chartTop;
       }
     }
 
     return y;
   }
 
-  function getCardHeight(d) {
-    let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
-    let h = cardHeight;
+  getCardHeight(d) {
+    let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding;
+    let h = this.cardHeight;
 
-    if (panel.yAxis.logBase !== 1 && d.y === 0) {
-      return cardHeight;
+    if (this.panel.yAxis.logBase !== 1 && d.y === 0) {
+      return this.cardHeight;
     }
 
     // Cut card height to prevent overlay
-    if (y < chartTop) {
-      h = yScale(d.y) - cardPadding;
-    } else if (yScale(d.y) > chartBottom) {
-      h = chartBottom - y;
-    } else if (y + cardHeight > chartBottom) {
-      h = chartBottom - y;
+    if (y < this.chartTop) {
+      h = this.yScale(d.y) - this.cardPadding;
+    } else if (this.yScale(d.y) > this.chartBottom) {
+      h = this.chartBottom - y;
+    } else if (y + this.cardHeight > this.chartBottom) {
+      h = this.chartBottom - y;
     }
 
     // Height can't be more than chart height
-    h = Math.min(h, chartHeight);
+    h = Math.min(h, this.chartHeight);
     // Card height should be MIN_CARD_SIZE at least
     h = Math.max(h, MIN_CARD_SIZE);
 
     return h;
   }
 
-  function getCardColor(d) {
-    if (panel.color.mode === 'opacity') {
-      return panel.color.cardColor;
+  getCardColor(d) {
+    if (this.panel.color.mode === 'opacity') {
+      return this.panel.color.cardColor;
     } else {
-      return colorScale(d.count);
+      return this.colorScale(d.count);
     }
   }
 
-  function getCardOpacity(d) {
-    if (panel.color.mode === 'opacity') {
-      return opacityScale(d.count);
+  getCardOpacity(d) {
+    if (this.panel.color.mode === 'opacity') {
+      return this.opacityScale(d.count);
     } else {
       return 1;
     }
   }
 
-  /////////////////////////////
-  // Selection and crosshair //
-  /////////////////////////////
+  onMouseDown(event) {
+    this.selection.active = true;
+    this.selection.x1 = event.offsetX;
 
-  // Shared crosshair and tooltip
-  appEvents.on(
-    'graph-hover',
-    event => {
-      drawSharedCrosshair(event.pos);
-    },
-    scope
-  );
-
-  appEvents.on(
-    'graph-hover-clear',
-    () => {
-      clearCrosshair();
-    },
-    scope
-  );
-
-  function onMouseDown(event) {
-    selection.active = true;
-    selection.x1 = event.offsetX;
-
-    mouseUpHandler = function() {
-      onMouseUp();
+    this.mouseUpHandler = () => {
+      this.onMouseUp();
     };
 
-    $(document).one('mouseup', mouseUpHandler);
+    $(document).one('mouseup', this.mouseUpHandler);
   }
 
-  function onMouseUp() {
-    $(document).unbind('mouseup', mouseUpHandler);
-    mouseUpHandler = null;
-    selection.active = false;
+  onMouseUp() {
+    $(document).unbind('mouseup', this.mouseUpHandler);
+    this.mouseUpHandler = null;
+    this.selection.active = false;
 
-    let selectionRange = Math.abs(selection.x2 - selection.x1);
-    if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
-      let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth);
-      let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth);
+    let selectionRange = Math.abs(this.selection.x2 - this.selection.x1);
+    if (this.selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
+      let timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth);
+      let timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth);
 
-      ctrl.timeSrv.setTime({
+      this.ctrl.timeSrv.setTime({
         from: moment.utc(timeFrom),
         to: moment.utc(timeTo),
       });
     }
 
-    clearSelection();
+    this.clearSelection();
   }
 
-  function onMouseLeave() {
+  onMouseLeave() {
     appEvents.emit('graph-hover-clear');
-    clearCrosshair();
+    this.clearCrosshair();
   }
 
-  function onMouseMove(event) {
-    if (!heatmap) {
+  onMouseMove(event) {
+    if (!this.heatmap) {
       return;
     }
 
-    if (selection.active) {
+    if (this.selection.active) {
       // Clear crosshair and tooltip
-      clearCrosshair();
-      tooltip.destroy();
+      this.clearCrosshair();
+      this.tooltip.destroy();
 
-      selection.x2 = limitSelection(event.offsetX);
-      drawSelection(selection.x1, selection.x2);
+      this.selection.x2 = this.limitSelection(event.offsetX);
+      this.drawSelection(this.selection.x1, this.selection.x2);
     } else {
-      emitGraphHoverEvent(event);
-      drawCrosshair(event.offsetX);
-      tooltip.show(event, data);
+      this.emitGraphHoverEvent(event);
+      this.drawCrosshair(event.offsetX);
+      this.tooltip.show(event, this.data);
     }
   }
 
-  function emitGraphHoverEvent(event) {
-    let x = xScale.invert(event.offsetX - yAxisWidth).valueOf();
-    let y = yScale.invert(event.offsetY);
+  emitGraphHoverEvent(event) {
+    let x = this.xScale.invert(event.offsetX - this.yAxisWidth).valueOf();
+    let y = this.yScale.invert(event.offsetY);
     let pos = {
       pageX: event.pageX,
       pageY: event.pageY,
@@ -730,105 +743,100 @@ export default function link(scope, elem, attrs, ctrl) {
     };
 
     // Set minimum offset to prevent showing legend from another panel
-    pos.panelRelY = Math.max(event.offsetY / height, 0.001);
+    pos.panelRelY = Math.max(event.offsetY / this.height, 0.001);
 
     // broadcast to other graph panels that we are hovering
-    appEvents.emit('graph-hover', { pos: pos, panel: panel });
+    appEvents.emit('graph-hover', { pos: pos, panel: this.panel });
   }
 
-  function limitSelection(x2) {
-    x2 = Math.max(x2, yAxisWidth);
-    x2 = Math.min(x2, chartWidth + yAxisWidth);
+  limitSelection(x2) {
+    x2 = Math.max(x2, this.yAxisWidth);
+    x2 = Math.min(x2, this.chartWidth + this.yAxisWidth);
     return x2;
   }
 
-  function drawSelection(posX1, posX2) {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-selection').remove();
+  drawSelection(posX1, posX2) {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-selection').remove();
       let selectionX = Math.min(posX1, posX2);
       let selectionWidth = Math.abs(posX1 - posX2);
 
       if (selectionWidth > MIN_SELECTION_WIDTH) {
-        heatmap
+        this.heatmap
           .append('rect')
           .attr('class', 'heatmap-selection')
           .attr('x', selectionX)
           .attr('width', selectionWidth)
-          .attr('y', chartTop)
-          .attr('height', chartHeight);
+          .attr('y', this.chartTop)
+          .attr('height', this.chartHeight);
       }
     }
   }
 
-  function clearSelection() {
-    selection.x1 = -1;
-    selection.x2 = -1;
+  clearSelection() {
+    this.selection.x1 = -1;
+    this.selection.x2 = -1;
 
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-selection').remove();
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-selection').remove();
     }
   }
 
-  function drawCrosshair(position) {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-crosshair').remove();
+  drawCrosshair(position) {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-crosshair').remove();
 
       let posX = position;
-      posX = Math.max(posX, yAxisWidth);
-      posX = Math.min(posX, chartWidth + yAxisWidth);
+      posX = Math.max(posX, this.yAxisWidth);
+      posX = Math.min(posX, this.chartWidth + this.yAxisWidth);
 
-      heatmap
+      this.heatmap
         .append('g')
         .attr('class', 'heatmap-crosshair')
         .attr('transform', 'translate(' + posX + ',0)')
         .append('line')
         .attr('x1', 1)
-        .attr('y1', chartTop)
+        .attr('y1', this.chartTop)
         .attr('x2', 1)
-        .attr('y2', chartBottom)
+        .attr('y2', this.chartBottom)
         .attr('stroke-width', 1);
     }
   }
 
-  function drawSharedCrosshair(pos) {
-    if (heatmap && ctrl.dashboard.graphTooltip !== 0) {
-      let posX = xScale(pos.x) + yAxisWidth;
-      drawCrosshair(posX);
+  drawSharedCrosshair(pos) {
+    if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) {
+      let posX = this.xScale(pos.x) + this.yAxisWidth;
+      this.drawCrosshair(posX);
     }
   }
 
-  function clearCrosshair() {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-crosshair').remove();
+  clearCrosshair() {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-crosshair').remove();
     }
   }
 
-  function render() {
-    data = ctrl.data;
-    panel = ctrl.panel;
-    timeRange = ctrl.range;
+  render() {
+    this.data = this.ctrl.data;
+    this.panel = this.ctrl.panel;
+    this.timeRange = this.ctrl.range;
 
-    if (!setElementHeight() || !data) {
+    if (!this.setElementHeight() || !this.data) {
       return;
     }
 
     // Draw default axes and return if no data
-    if (_.isEmpty(data.buckets)) {
-      addHeatmapCanvas();
-      addAxes();
+    if (_.isEmpty(this.data.buckets)) {
+      this.addHeatmapCanvas();
+      this.addAxes();
       return;
     }
 
-    addHeatmap();
-    scope.yAxisWidth = yAxisWidth;
-    scope.xAxisHeight = xAxisHeight;
-    scope.chartHeight = chartHeight;
-    scope.chartWidth = chartWidth;
-    scope.chartTop = chartTop;
+    this.addHeatmap();
+    this.scope.yAxisWidth = this.yAxisWidth;
+    this.scope.xAxisHeight = this.xAxisHeight;
+    this.scope.chartHeight = this.chartHeight;
+    this.scope.chartWidth = this.chartWidth;
+    this.scope.chartTop = this.chartTop;
   }
-
-  // Register selection listeners
-  $heatmap.on('mousedown', onMouseDown);
-  $heatmap.on('mousemove', onMouseMove);
-  $heatmap.on('mouseleave', onMouseLeave);
 }

From 520aad819d8b43fce404ab3452068605f044c48a Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 10 Aug 2018 16:30:51 +0200
Subject: [PATCH 281/380] Replace element

---
 .../panel/heatmap/specs/renderer.jest.ts      | 23 +++++++++++--------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
index 4e0e8d1b6a9..7001134bd70 100644
--- a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
+++ b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
@@ -13,6 +13,8 @@ import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSi
 describe('grafanaHeatmap', function() {
   //   beforeEach(angularMocks.module('grafana.core'));
 
+  let scope = <any>{};
+
   function heatmapScenario(desc, func, elementWidth = 500) {
     describe(desc, function() {
       var ctx: any = {};
@@ -89,7 +91,7 @@ describe('grafanaHeatmap', function() {
             },
           };
 
-          var scope = $rootScope.$new();
+          // var scope = $rootScope.$new();
           scope.ctrl = ctrl;
 
           ctx.series = [];
@@ -131,20 +133,21 @@ describe('grafanaHeatmap', function() {
           ctx.data.cards = cards;
           ctx.data.cardStats = cardStats;
 
-          let elemHtml = `
-          <div class="heatmap-wrapper">
-            <div class="heatmap-canvas-wrapper">
-              <div class="heatmap-panel" style='width:${elementWidth}px'></div>
-            </div>
-          </div>`;
+          // let elemHtml = `
+          // <div class="heatmap-wrapper">
+          //   <div class="heatmap-canvas-wrapper">
+          //     <div class="heatmap-panel" style='width:${elementWidth}px'></div>
+          //   </div>
+          // </div>`;
 
-          var element = $.parseHTML(elemHtml);
+          // var element = $.parseHTML(elemHtml);
           // $compile(element)(scope);
           // scope.$digest();
 
           ctrl.data = ctx.data;
-          ctx.element = element;
-          rendering(scope, $(element), [], ctrl);
+          // ctx.element = element;
+          let elem = {};
+          let render = new rendering(scope, elem, [], ctrl);
           ctrl.events.emit('render');
         });
       };

From 8d2aac09366ba674663761cb16af31f319ab174c Mon Sep 17 00:00:00 2001
From: Ali Anwar <mynameisalianwar@gmail.com>
Date: Sat, 11 Aug 2018 23:42:31 -0700
Subject: [PATCH 282/380] Fix typo

---
 docs/sources/http_api/folder.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md
index fb318ecf58e..e8845c3b125 100644
--- a/docs/sources/http_api/folder.md
+++ b/docs/sources/http_api/folder.md
@@ -223,7 +223,7 @@ Status Codes:
 - **404** – Folder not found
 - **412** – Precondition failed
 
-The **412** status code is used for explaing that you cannot update the folder and why.
+The **412** status code is used for explaining that you cannot update the folder and why.
 There can be different reasons for this:
 
 - The folder has been changed by someone else, `status=version-mismatch`

From 5fd8849d656d4ee90d24c394924010ce49f8089d Mon Sep 17 00:00:00 2001
From: Ali Anwar <anwar1@berkeley.edu>
Date: Sat, 11 Aug 2018 23:44:15 -0700
Subject: [PATCH 283/380] Update dashboard.md

---
 docs/sources/http_api/dashboard.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md
index ea1bd7f2ef7..3df36894901 100644
--- a/docs/sources/http_api/dashboard.md
+++ b/docs/sources/http_api/dashboard.md
@@ -85,7 +85,7 @@ Status Codes:
 - **403** – Access denied
 - **412** – Precondition failed
 
-The **412** status code is used for explaing that you cannot create the dashboard and why.
+The **412** status code is used for explaining that you cannot create the dashboard and why.
 There can be different reasons for this:
 
 - The dashboard has been changed by someone else, `status=version-mismatch`

From d81a23becf9b306ff7dbf473a3089e8468868135 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Sun, 12 Aug 2018 10:51:58 +0200
Subject: [PATCH 284/380] Refactor setting fillmode

This adds SetupFillmode to the tsdb package to be used by the sql
datasources.
---
 pkg/tsdb/mssql/macros.go    | 19 +++----------------
 pkg/tsdb/mysql/macros.go    | 18 +++---------------
 pkg/tsdb/postgres/macros.go | 18 +++---------------
 pkg/tsdb/sql_engine.go      | 21 +++++++++++++++++++++
 4 files changed, 30 insertions(+), 46 deletions(-)

diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index 42e47ce6d3c..920e3781e0c 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -6,8 +6,6 @@ import (
 	"strings"
 	"time"
 
-	"strconv"
-
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
@@ -97,20 +95,9 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			return "", fmt.Errorf("error parsing interval %v", args[1])
 		}
 		if len(args) == 3 {
-			m.query.Model.Set("fill", true)
-			m.query.Model.Set("fillInterval", interval.Seconds())
-			switch args[2] {
-			case "NULL":
-				m.query.Model.Set("fillMode", "null")
-			case "previous":
-				m.query.Model.Set("fillMode", "previous")
-			default:
-				m.query.Model.Set("fillMode", "value")
-				floatVal, err := strconv.ParseFloat(args[2], 64)
-				if err != nil {
-					return "", fmt.Errorf("error parsing fill value %v", args[2])
-				}
-				m.query.Model.Set("fillValue", floatVal)
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
 			}
 		}
 		return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index 905d424f29a..48fa193edd5 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -3,7 +3,6 @@ package mysql
 import (
 	"fmt"
 	"regexp"
-	"strconv"
 	"strings"
 	"time"
 
@@ -92,20 +91,9 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			return "", fmt.Errorf("error parsing interval %v", args[1])
 		}
 		if len(args) == 3 {
-			m.query.Model.Set("fill", true)
-			m.query.Model.Set("fillInterval", interval.Seconds())
-			switch args[2] {
-			case "NULL":
-				m.query.Model.Set("fillMode", "null")
-			case "previous":
-				m.query.Model.Set("fillMode", "previous")
-			default:
-				m.query.Model.Set("fillMode", "value")
-				floatVal, err := strconv.ParseFloat(args[2], 64)
-				if err != nil {
-					return "", fmt.Errorf("error parsing fill value %v", args[2])
-				}
-				m.query.Model.Set("fillValue", floatVal)
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
 			}
 		}
 		return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index aebdc55d1d7..a4b4aaa9d1e 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -3,7 +3,6 @@ package postgres
 import (
 	"fmt"
 	"regexp"
-	"strconv"
 	"strings"
 	"time"
 
@@ -114,20 +113,9 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			return "", fmt.Errorf("error parsing interval %v", args[1])
 		}
 		if len(args) == 3 {
-			m.query.Model.Set("fill", true)
-			m.query.Model.Set("fillInterval", interval.Seconds())
-			switch args[2] {
-			case "NULL":
-				m.query.Model.Set("fillMode", "null")
-			case "previous":
-				m.query.Model.Set("fillMode", "previous")
-			default:
-				m.query.Model.Set("fillMode", "value")
-				floatVal, err := strconv.ParseFloat(args[2], 64)
-				if err != nil {
-					return "", fmt.Errorf("error parsing fill value %v", args[2])
-				}
-				m.query.Model.Set("fillValue", floatVal)
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
 			}
 		}
 		return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index cbf6d6b4d60..454853c7cc8 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -6,6 +6,7 @@ import (
 	"database/sql"
 	"fmt"
 	"math"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -568,3 +569,23 @@ func ConvertSqlValueColumnToFloat(columnName string, columnValue interface{}) (n
 
 	return value, nil
 }
+
+func SetupFillmode(query *Query, interval time.Duration, fillmode string) error {
+	query.Model.Set("fill", true)
+	query.Model.Set("fillInterval", interval.Seconds())
+	switch fillmode {
+	case "NULL":
+		query.Model.Set("fillMode", "null")
+	case "previous":
+		query.Model.Set("fillMode", "previous")
+	default:
+		query.Model.Set("fillMode", "value")
+		floatVal, err := strconv.ParseFloat(fillmode, 64)
+		if err != nil {
+			return fmt.Errorf("error parsing fill value %v", fillmode)
+		}
+		query.Model.Set("fillValue", floatVal)
+	}
+
+	return nil
+}

From 48364f0111cfdaacfd4a05eaf4da98ba94a00251 Mon Sep 17 00:00:00 2001
From: Julien Pivotto <roidelapluie@gmail.com>
Date: Mon, 13 Aug 2018 07:53:41 +0200
Subject: [PATCH 285/380] Add support for $__range_s (#12883)

Fixes #12882

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
---
 docs/sources/features/datasources/prometheus.md           | 8 ++++----
 docs/sources/reference/templating.md                      | 2 +-
 public/app/plugins/datasource/prometheus/datasource.ts    | 2 ++
 .../datasource/prometheus/specs/datasource.jest.ts        | 2 ++
 4 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md
index 3a04ef92e31..611a3b4d9e2 100644
--- a/docs/sources/features/datasources/prometheus.md
+++ b/docs/sources/features/datasources/prometheus.md
@@ -78,9 +78,9 @@ For details of *metric names*, *label names* and *label values* are please refer
 
 #### Using interval and range variables
 
-> Support for `$__range` and `$__range_ms` only available from Grafana v5.3
+> Support for `$__range`, `$__range_s` and `$__range_ms` only available from Grafana v5.3
 
-It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since
+It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range`, `$__range_s` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since
 `label_values` function doesn't support queries.
 
 Make sure to set the variable's `refresh` trigger to be `On Time Range Change` to get the correct instances when changing the time range on the dashboard.
@@ -94,10 +94,10 @@ Query: query_result(topk(5, sum(rate(http_requests_total[$__range])) by (instanc
 Regex: /"([^"]+)"/
 ```
 
-Populate a variable with the instances having a certain state over the time range shown in the dashboard:
+Populate a variable with the instances having a certain state over the time range shown in the dashboard, using the more precise `$__range_s`:
 
 ```
-Query: query_result(max_over_time(<metric>[$__range]) != <state>)
+Query: query_result(max_over_time(<metric>[${__range_s}s]) != <state>)
 Regex:
 ```
 
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index ce1a1299d26..d04d56dc788 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -277,7 +277,7 @@ This variable is only available in the Singlestat panel and can be used in the p
 
 > Only available in Grafana v5.3+
 
-Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond representation called `$__range_ms`.
+Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond and a second representation called `$__range_ms` and `$__range_s`.
 
 ## Repeating Panels
 
diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index ef440ab515d..318b0f8f1fc 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -489,9 +489,11 @@ export class PrometheusDatasource {
   getRangeScopedVars() {
     let range = this.timeSrv.timeRange();
     let msRange = range.to.diff(range.from);
+    let sRange = Math.round(msRange / 1000);
     let regularRange = kbn.secondsToHms(msRange / 1000);
     return {
       __range_ms: { text: msRange, value: msRange },
+      __range_s: { text: sRange, value: sRange },
       __range: { text: regularRange, value: regularRange },
     };
   }
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index a108909e6e1..4ba2e3260a7 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -321,8 +321,10 @@ describe('PrometheusDatasource', () => {
     it('should have the correct range and range_ms', () => {
       let range = ctx.templateSrvMock.replace.mock.calls[0][1].__range;
       let rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms;
+      let rangeS = ctx.templateSrvMock.replace.mock.calls[0][1].__range_s;
       expect(range).toEqual({ text: '21s', value: '21s' });
       expect(rangeMs).toEqual({ text: 21031, value: 21031 });
+      expect(rangeS).toEqual({ text: 21, value: 21 });
     });
 
     it('should pass the default interval value', () => {

From 974359534fac6e91165b05705846ab225e4867d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 13 Aug 2018 07:54:49 +0200
Subject: [PATCH 286/380] Update CHANGELOG.md

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 198b28ca392..ef0d5b98696 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,7 +14,7 @@
 * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
-* **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
+* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms  support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)

From 48713b76f335bc307e6648985c26717287249bed Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Thu, 9 Aug 2018 16:29:16 +0200
Subject: [PATCH 287/380] docker: makes it possible to set a specific plugin
 url.

Originally from the grafana/grafana-docker repo, authored
by @ClementGautier.
---
 packaging/docker/run.sh | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh
index 2d2318a9210..bc001bdf90a 100755
--- a/packaging/docker/run.sh
+++ b/packaging/docker/run.sh
@@ -67,7 +67,13 @@ if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then
   IFS=','
   for plugin in ${GF_INSTALL_PLUGINS}; do
     IFS=$OLDIFS
-    grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}
+    if [[ $plugin =~ .*\;.* ]]; then
+        pluginUrl=$(echo "$plugin" | cut -d';' -f 1)
+        pluginWithoutUrl=$(echo "$plugin" | cut -d';' -f 2)
+        grafana-cli --pluginUrl "${pluginUrl}" --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${pluginWithoutUrl}
+    else
+        grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}
+    fi
   done
 fi
 

From aeba01237d3763c3a2560304bb19365d43167901 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 13 Aug 2018 09:20:17 +0200
Subject: [PATCH 288/380] Changelog update

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef0d5b98696..6d1816b56b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,7 @@
 * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
 * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
 * **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud)
+* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
 
 ### Breaking changes
 

From a79c43420a54cad877d51d27cb5812a1fd3a3b02 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 13 Aug 2018 10:57:32 +0200
Subject: [PATCH 289/380] Add mocks

---
 .../app/plugins/panel/heatmap/heatmap_ctrl.ts |  2 +-
 public/app/plugins/panel/heatmap/rendering.ts | 32 +++++++++++--------
 .../panel/heatmap/specs/renderer.jest.ts      | 24 ++++++++++----
 3 files changed, 36 insertions(+), 22 deletions(-)

diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
index 1749403edf0..1d35ff2ea84 100644
--- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
+++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts
@@ -358,6 +358,6 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
   }
 
   link(scope, elem, attrs, ctrl) {
-    let render = new rendering(scope, elem, attrs, ctrl);
+    rendering(scope, elem, attrs, ctrl);
   }
 }
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index d54eb5750cd..5af916ac13e 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/public/app/plugins/panel/heatmap/rendering.ts
@@ -19,7 +19,10 @@ let MIN_CARD_SIZE = 1,
   Y_AXIS_TICK_PADDING = 5,
   MIN_SELECTION_WIDTH = 2;
 
-export default class Link {
+export default function rendering(scope, elem, attrs, ctrl) {
+  return new Link(scope, elem, attrs, ctrl);
+}
+export class Link {
   width: number;
   height: number;
   yScale: any;
@@ -50,7 +53,7 @@ export default class Link {
   dataRangeWidingFactor: number;
   constructor(private scope, private elem, attrs, private ctrl) {
     // $heatmap is JQuery object, but heatmap is D3
-    this.$heatmap = elem.find('.heatmap-panel');
+    this.$heatmap = this.elem.find('.heatmap-panel');
     this.tooltip = new HeatmapTooltip(this.$heatmap, this.scope);
 
     this.selection = {
@@ -65,7 +68,7 @@ export default class Link {
 
     this.ctrl.events.on('render', this.onRender.bind(this));
 
-    this.ctrl.tickValueFormatter = this.tickValueFormatter;
+    this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this);
     /////////////////////////////
     // Selection and crosshair //
     /////////////////////////////
@@ -151,7 +154,7 @@ export default class Link {
     } else {
       timeFormat = d3.timeFormat(grafanaTimeFormatter);
     }
-
+    console.log(ticks);
     let xAxis = d3
       .axisBottom(this.xScale)
       .ticks(ticks)
@@ -345,11 +348,12 @@ export default class Link {
     const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
     this.ctrl.decimals = decimals;
 
+    let tickValueFormatter = this.tickValueFormatter.bind(this);
     function tickFormatter(valIndex) {
       let valueFormatted = tsBuckets[valIndex];
       if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
         // Try to format numeric tick labels
-        valueFormatted = this.tickValueFormatter(decimals)(_.toNumber(valueFormatted));
+        valueFormatted = tickValueFormatter(decimals)(_.toNumber(valueFormatted));
       }
       return valueFormatted;
     }
@@ -533,17 +537,17 @@ export default class Link {
     cards = cards
       .enter()
       .append('rect')
-      .attr('x', this.getCardX)
-      .attr('width', this.getCardWidth)
-      .attr('y', this.getCardY)
-      .attr('height', this.getCardHeight)
+      .attr('x', this.getCardX.bind(this))
+      .attr('width', this.getCardWidth.bind(this))
+      .attr('y', this.getCardY.bind(this))
+      .attr('height', this.getCardHeight.bind(this))
       .attr('rx', this.cardRound)
       .attr('ry', this.cardRound)
       .attr('class', 'bordered heatmap-card')
-      .style('fill', this.getCardColor)
-      .style('stroke', this.getCardColor)
+      .style('fill', this.getCardColor.bind(this))
+      .style('stroke', this.getCardColor.bind(this))
       .style('stroke-width', 0)
-      .style('opacity', this.getCardOpacity);
+      .style('opacity', this.getCardOpacity.bind(this));
 
     let $cards = this.$heatmap.find('.heatmap-card');
     $cards
@@ -683,11 +687,11 @@ export default class Link {
       this.onMouseUp();
     };
 
-    $(document).one('mouseup', this.mouseUpHandler);
+    $(document).one('mouseup', this.mouseUpHandler.bind(this));
   }
 
   onMouseUp() {
-    $(document).unbind('mouseup', this.mouseUpHandler);
+    $(document).unbind('mouseup', this.mouseUpHandler.bind(this));
     this.mouseUpHandler = null;
     this.selection.active = false;
 
diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
index 7001134bd70..c660761890c 100644
--- a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
+++ b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
@@ -1,14 +1,19 @@
 // import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
 
 import '../module';
-import angular from 'angular';
-import $ from 'jquery';
+// import angular from 'angular';
+// import $ from 'jquery';
 // import helpers from 'test/specs/helpers';
 import TimeSeries from 'app/core/time_series2';
 import moment from 'moment';
-import { Emitter } from 'app/core/core';
+// import { Emitter } from 'app/core/core';
 import rendering from '../rendering';
 import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter';
+jest.mock('app/core/core', () => ({
+  appEvents: {
+    on: () => {},
+  },
+}));
 
 describe('grafanaHeatmap', function() {
   //   beforeEach(angularMocks.module('grafana.core'));
@@ -37,7 +42,10 @@ describe('grafanaHeatmap', function() {
               },
               { name: 'Reds', value: 'interpolateReds', invert: 'dark' },
             ],
-            //   events: new Emitter(),
+            events: {
+              on: () => {},
+              emit: () => {},
+            },
             height: 200,
             panel: {
               heatmap: {},
@@ -145,9 +153,11 @@ describe('grafanaHeatmap', function() {
           // scope.$digest();
 
           ctrl.data = ctx.data;
-          // ctx.element = element;
-          let elem = {};
-          let render = new rendering(scope, elem, [], ctrl);
+          ctx.element = {
+            find: () => ({ on: () => {} }),
+            on: () => {},
+          };
+          rendering(scope, ctx.element, [], ctrl);
           ctrl.events.emit('render');
         });
       };

From d7a0f5ee074caaed6eb8884c537d4681230518cf Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Mon, 13 Aug 2018 11:14:24 +0200
Subject: [PATCH 290/380] Removes link to deprecated docker image build

---
 packaging/docker/README.md | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/packaging/docker/README.md b/packaging/docker/README.md
index d80cd87aebc..cfb3c7248ef 100644
--- a/packaging/docker/README.md
+++ b/packaging/docker/README.md
@@ -1,7 +1,5 @@
 # Grafana Docker image
 
-[![CircleCI](https://circleci.com/gh/grafana/grafana-docker.svg?style=svg)](https://circleci.com/gh/grafana/grafana-docker)
-
 ## Running your Grafana container
 
 Start your container binding the external port `3000`.
@@ -42,4 +40,4 @@ Further documentation can be found at http://docs.grafana.org/installation/docke
 * Plugins dir (`/var/lib/grafana/plugins`) is no longer a separate volume
 
 ### v3.1.1
-* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026
\ No newline at end of file
+* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026

From edb34a36a0cb84c0b6ec02bde71b68fddea0d6ca Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 13 Aug 2018 11:16:49 +0200
Subject: [PATCH 291/380] changelog: add notes about closing #12882

[skip ci]
---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d1816b56b2..7d5ed3378de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,7 +14,7 @@
 * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
-* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms  support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
+* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)

From bdd9af0864adc5dd169c9349eae25e574f8c2937 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Mon, 13 Aug 2018 11:34:16 +0200
Subject: [PATCH 292/380] changed const members to filteredMembers to trigger
 get filtered members, changed input value to team.search (#12885)

---
 public/app/containers/Teams/TeamMembers.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/containers/Teams/TeamMembers.tsx
index 88933e00ab1..a6b0b04f19d 100644
--- a/public/app/containers/Teams/TeamMembers.tsx
+++ b/public/app/containers/Teams/TeamMembers.tsx
@@ -69,8 +69,9 @@ export class TeamMembers extends React.Component<Props, State> {
 
   render() {
     const { newTeamMember, isAdding } = this.state;
-    const members = this.props.team.members.values();
+    const members = this.props.team.filteredMembers;
     const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
+    const { team } = this.props;
 
     return (
       <div>
@@ -81,7 +82,7 @@ export class TeamMembers extends React.Component<Props, State> {
                 type="text"
                 className="gf-form-input"
                 placeholder="Search members"
-                value={''}
+                value={team.search}
                 onChange={this.onSearchQueryChange}
               />
               <i className="gf-form-input-icon fa fa-search" />

From bfe28ee061ea42b27057c582f0b436cf12c46e88 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Mon, 13 Aug 2018 12:08:14 +0200
Subject: [PATCH 293/380] Add $__unixEpochGroup macro to postgres datasource

---
 docs/sources/features/datasources/postgres.md |  2 ++
 pkg/tsdb/postgres/macros.go                   | 21 +++++++++++++++++++
 pkg/tsdb/postgres/macros_test.go              | 12 +++++++++++
 .../postgres/partials/query.editor.html       |  2 ++
 4 files changed, 37 insertions(+)

diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index 2be2db0837b..cf77643f06b 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -68,6 +68,8 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index a4b4aaa9d1e..d2a3d599441 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -134,6 +134,27 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
 		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+	case "__unixEpochGroup":
+		if len(args) < 2 {
+			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+		}
+		interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+		if err != nil {
+			return "", fmt.Errorf("error parsing interval %v", args[1])
+		}
+		if len(args) == 3 {
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
+			}
+		}
+		return fmt.Sprintf("floor(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__unixEpochGroupAlias":
+		tg, err := m.evaluateMacro("__unixEpochGroup", args)
+		if err == nil {
+			return tg + " AS \"time\"", err
+		}
+		return "", err
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index beeea93893b..a029fc49ee0 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -110,6 +110,18 @@ func TestMacroEngine(t *testing.T) {
 
 				So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
 			})
+
+			Convey("interpolate __unixEpochGroup function", func() {
+
+				sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "SELECT floor(time_column/300)*300")
+				So(sql2, ShouldEqual, sql+" AS \"time\"")
+			})
+
 		})
 
 		Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html
index 20353b81ba2..763fd6a6e96 100644
--- a/public/app/plugins/datasource/postgres/partials/query.editor.html
+++ b/public/app/plugins/datasource/postgres/partials/query.editor.html
@@ -57,6 +57,8 @@ Macros:
      by setting fillvalue grafana will fill in missing values according to the interval
      fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column,'5m') -&gt; (extract(epoch from column)/300)::bigint*300 AS "time"
+- $__unixEpochGroup(column,'5m') -&gt; floor(column/300)*300
+- $__unixEpochGroupAlias(column,'5m') -&gt; floor(column/300)*300 AS "time"
 
 Example of group by and order by with $__timeGroup:
 SELECT

From fbc67a1c64a0a94d169aea63aa00c0f1055dfc6d Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Mon, 13 Aug 2018 12:17:05 +0200
Subject: [PATCH 294/380] add $__unixEpochGroup to mysql datasource

---
 docs/sources/features/datasources/mysql.md    |  2 ++
 pkg/tsdb/mysql/macros.go                      | 21 +++++++++++++++++++
 pkg/tsdb/mysql/macros_test.go                 | 12 +++++++++++
 .../mysql/partials/query.editor.html          |  2 ++
 4 files changed, 37 insertions(+)

diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index cdb78deed35..afac746b050 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -71,6 +71,8 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 
diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index 48fa193edd5..0dabdd7c283 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -112,6 +112,27 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
 		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+	case "__unixEpochGroup":
+		if len(args) < 2 {
+			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+		}
+		interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+		if err != nil {
+			return "", fmt.Errorf("error parsing interval %v", args[1])
+		}
+		if len(args) == 3 {
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
+			}
+		}
+		return fmt.Sprintf("%s DIV %v * %v", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__unixEpochGroupAlias":
+		tg, err := m.evaluateMacro("__unixEpochGroup", args)
+		if err == nil {
+			return tg + " AS \"time\"", err
+		}
+		return "", err
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}
diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go
index fd9d3f5688a..fe153ca3e2d 100644
--- a/pkg/tsdb/mysql/macros_test.go
+++ b/pkg/tsdb/mysql/macros_test.go
@@ -97,6 +97,18 @@ func TestMacroEngine(t *testing.T) {
 
 				So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
 			})
+
+			Convey("interpolate __unixEpochGroup function", func() {
+
+				sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "SELECT time_column DIV 300 * 300")
+				So(sql2, ShouldEqual, sql+" AS \"time\"")
+			})
+
 		})
 
 		Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html
index 7c799eec21b..1e829a1175d 100644
--- a/public/app/plugins/datasource/mysql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mysql/partials/query.editor.html
@@ -57,6 +57,8 @@ Macros:
      by setting fillvalue grafana will fill in missing values according to the interval
      fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time"
+- $__unixEpochGroup(column,'5m') -&gt; column DIV 300 * 300
+- $__unixEpochGroupAlias(column,'5m') -&gt; column DIV 300 * 300 AS "time"
 
 Example of group by and order by with $__timeGroup:
 SELECT

From 8c4d59363e6aabd9cb772af41569f13e64951691 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Mon, 13 Aug 2018 12:23:42 +0200
Subject: [PATCH 295/380] add $__unixEpochGroup to mssql datasource

---
 docs/sources/features/datasources/mssql.md    |  2 ++
 pkg/tsdb/mssql/macros.go                      | 21 +++++++++++++++++++
 pkg/tsdb/mssql/macros_test.go                 | 12 +++++++++++
 .../mssql/partials/query.editor.html          |  2 ++
 4 files changed, 37 insertions(+)

diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md
index caaf5a6b321..da0c9581e99 100644
--- a/docs/sources/features/datasources/mssql.md
+++ b/docs/sources/features/datasources/mssql.md
@@ -88,6 +88,8 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
+*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 
 We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
 
diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index 920e3781e0c..caba043e7b6 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -116,6 +116,27 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 		return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil
 	case "__unixEpochTo":
 		return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil
+	case "__unixEpochGroup":
+		if len(args) < 2 {
+			return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
+		}
+		interval, err := time.ParseDuration(strings.Trim(args[1], `'`))
+		if err != nil {
+			return "", fmt.Errorf("error parsing interval %v", args[1])
+		}
+		if len(args) == 3 {
+			err := tsdb.SetupFillmode(m.query, interval, args[2])
+			if err != nil {
+				return "", err
+			}
+		}
+		return fmt.Sprintf("FLOOR(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+	case "__unixEpochGroupAlias":
+		tg, err := m.evaluateMacro("__unixEpochGroup", args)
+		if err == nil {
+			return tg + " AS [time]", err
+		}
+		return "", err
 	default:
 		return "", fmt.Errorf("Unknown macro %v", name)
 	}
diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go
index 8362ae05aa6..8e0973b750c 100644
--- a/pkg/tsdb/mssql/macros_test.go
+++ b/pkg/tsdb/mssql/macros_test.go
@@ -145,6 +145,18 @@ func TestMacroEngine(t *testing.T) {
 
 				So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
 			})
+
+			Convey("interpolate __unixEpochGroup function", func() {
+
+				sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+				sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "SELECT FLOOR(time_column/300)*300")
+				So(sql2, ShouldEqual, sql+" AS [time]")
+			})
+
 		})
 
 		Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html
index 7888e36a24c..4b0a46b6412 100644
--- a/public/app/plugins/datasource/mssql/partials/query.editor.html
+++ b/public/app/plugins/datasource/mssql/partials/query.editor.html
@@ -57,6 +57,8 @@ Macros:
      by setting fillvalue grafana will fill in missing values according to the interval
      fillvalue can be either a literal value, NULL or previous; previous will fill in the previous seen value or NULL if none has been seen yet
 - $__timeGroupAlias(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time]
+- $__unixEpochGroup(column,'5m') -&gt; FLOOR(column/300)*300
+- $__unixEpochGroupAlias(column,'5m') -&gt; FLOOR(column/300)*300 AS [time]
 
 Example of group by and order by with $__timeGroup:
 SELECT

From 978e89657ecd4f8795721db2b9c21ea2ab1a0655 Mon Sep 17 00:00:00 2001
From: David Kaltschmidt <david.kaltschmidt@gmail.com>
Date: Mon, 13 Aug 2018 12:53:12 +0200
Subject: [PATCH 296/380] Explore: Fix label filtering for rate queries

- exclude `]` from match expression for selector injection to ignore
  range vectors like `[10m]`
---
 public/app/plugins/datasource/prometheus/datasource.ts          | 2 +-
 .../app/plugins/datasource/prometheus/specs/datasource.jest.ts  | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts
index 318b0f8f1fc..9d4d0433d5d 100644
--- a/public/app/plugins/datasource/prometheus/datasource.ts
+++ b/public/app/plugins/datasource/prometheus/datasource.ts
@@ -39,7 +39,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri
 
   // Add empty selector to bare metric name
   let previousWord;
-  query = query.replace(/(\w+)\b(?![\({=",])/g, (match, word, offset) => {
+  query = query.replace(/(\w+)\b(?![\(\]{=",])/g, (match, word, offset) => {
     // Check if inside a selector
     const nextSelectorStart = query.slice(offset).indexOf('{');
     const nextSelectorEnd = query.slice(offset).indexOf('}');
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
index 4ba2e3260a7..ed467c54b24 100644
--- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
+++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
@@ -351,6 +351,7 @@ describe('PrometheusDatasource', () => {
     expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe(
       'foo{bar="baz",instance="my-host.com:9100"}'
     );
+    expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])');
   });
 });
 

From 2e2de38b31918f704a0e76ec60e5d997e2ed0bb1 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 13 Aug 2018 13:55:47 +0200
Subject: [PATCH 297/380] Mock things

---
 public/app/plugins/panel/heatmap/rendering.ts |  2 +-
 .../panel/heatmap/specs/renderer.jest.ts      | 39 ++++++++++++++-----
 2 files changed, 30 insertions(+), 11 deletions(-)

diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index 5af916ac13e..e68d63cfbf8 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/public/app/plugins/panel/heatmap/rendering.ts
@@ -456,7 +456,6 @@ export class Link {
     this.chartHeight = this.height - this.margin.top - this.margin.bottom;
     this.chartTop = this.margin.top;
     this.chartBottom = this.chartTop + this.chartHeight;
-
     if (this.panel.dataFormat === 'tsbuckets') {
       this.addYAxisFromBuckets();
     } else {
@@ -550,6 +549,7 @@ export class Link {
       .style('opacity', this.getCardOpacity.bind(this));
 
     let $cards = this.$heatmap.find('.heatmap-card');
+    console.log($cards);
     $cards
       .on('mouseenter', event => {
         this.tooltip.mouseOverBucket = true;
diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
index c660761890c..a5546624d65 100644
--- a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
+++ b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
@@ -8,17 +8,24 @@ import TimeSeries from 'app/core/time_series2';
 import moment from 'moment';
 // import { Emitter } from 'app/core/core';
 import rendering from '../rendering';
+// import * as d3 from 'd3';
 import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter';
 jest.mock('app/core/core', () => ({
   appEvents: {
     on: () => {},
   },
+  contextSrv: {
+    user: {
+      lightTheme: false,
+    },
+  },
 }));
 
 describe('grafanaHeatmap', function() {
   //   beforeEach(angularMocks.module('grafana.core'));
 
   let scope = <any>{};
+  let render;
 
   function heatmapScenario(desc, func, elementWidth = 500) {
     describe(desc, function() {
@@ -154,11 +161,20 @@ describe('grafanaHeatmap', function() {
 
           ctrl.data = ctx.data;
           ctx.element = {
-            find: () => ({ on: () => {} }),
+            find: () => ({
+              on: () => {},
+              css: () => 189,
+              width: () => 189,
+              height: () => 200,
+              find: () => ({
+                on: () => {},
+              }),
+            }),
             on: () => {},
           };
-          rendering(scope, ctx.element, [], ctrl);
-          ctrl.events.emit('render');
+          render = rendering(scope, ctx.element, [], ctrl);
+          render.render();
+          render.ctrl.renderingCompleted();
         });
       };
 
@@ -172,6 +188,9 @@ describe('grafanaHeatmap', function() {
     });
 
     it('should draw correct Y axis', function() {
+      console.log('Runnign first test');
+      // console.log(render.ctrl.data);
+      console.log(render.scope.yScale);
       var yTicks = getTicks(ctx.element, '.axis-y');
       expect(yTicks).toEqual(['1', '2', '3']);
     });
@@ -317,13 +336,13 @@ describe('grafanaHeatmap', function() {
 });
 
 function getTicks(element, axisSelector) {
-  return element
-    .find(axisSelector)
-    .find('text')
-    .map(function() {
-      return this.textContent;
-    })
-    .get();
+  // return element
+  //   .find(axisSelector)
+  //   .find('text')
+  //   .map(function() {
+  //     return this.textContent;
+  //   })
+  //   .get();
 }
 
 function formatTime(timeStr) {

From e6057e08de4cddf5ba1a9f6c163f66835a15161b Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 13 Aug 2018 14:24:15 +0200
Subject: [PATCH 298/380] Rename to HeatmapRenderer

---
 public/app/plugins/panel/heatmap/rendering.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index e68d63cfbf8..6d3d21420e0 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/public/app/plugins/panel/heatmap/rendering.ts
@@ -20,9 +20,9 @@ let MIN_CARD_SIZE = 1,
   MIN_SELECTION_WIDTH = 2;
 
 export default function rendering(scope, elem, attrs, ctrl) {
-  return new Link(scope, elem, attrs, ctrl);
+  return new HeatmapRenderer(scope, elem, attrs, ctrl);
 }
-export class Link {
+export class HeatmapRenderer {
   width: number;
   height: number;
   yScale: any;

From 535bab1baaf45288e863fb04e89974f37b359421 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Mon, 13 Aug 2018 15:07:29 +0200
Subject: [PATCH 299/380] now hides team header when no teams + fix for list
 hidden when only one team

---
 public/app/features/org/partials/profile.html | 2 +-
 public/app/features/org/profile_ctrl.ts       | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html
index b204c223138..7858e00c683 100644
--- a/public/app/features/org/partials/profile.html
+++ b/public/app/features/org/partials/profile.html
@@ -26,7 +26,7 @@
 
   <prefs-control mode="user"></prefs-control>
 
-  <h3 class="page-heading">Teams</h3>
+  <h3 class="page-heading" ng-show="ctrl.showTeamsList">Teams</h3>
   <div class="gf-form-group" ng-show="ctrl.showTeamsList">
     <table class="filter-table form-inline">
       <thead>
diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts
index 6cfcdc2e64c..40ee4d908a1 100644
--- a/public/app/features/org/profile_ctrl.ts
+++ b/public/app/features/org/profile_ctrl.ts
@@ -30,7 +30,7 @@ export class ProfileCtrl {
   getUserTeams() {
     this.backendSrv.get('/api/user/teams').then(teams => {
       this.teams = teams;
-      this.showTeamsList = this.teams.length > 1;
+      this.showTeamsList = this.teams.length > 0;
     });
   }
 

From fd032c11111833bba562966a6379c4e20c102da6 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 13 Aug 2018 15:18:33 +0200
Subject: [PATCH 300/380] changelog: add notes about closing #12476

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d5ed3378de..0a36943af65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 * **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
 * **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
 * **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
+* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
 
 ### Minor
 

From f2b1fabd5c142d48f93f2499316d9a898fd09a0b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 13 Aug 2018 15:38:28 +0200
Subject: [PATCH 301/380] fix: Alerting rendering timeout was 30 seconds, same
 as alert rule eval timeout, this should be much lower so the rendering
 timeout does not timeout the rule context, fixes #12151 (#12903)

---
 pkg/services/alerting/notifier.go | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go
index 07212746f7e..f4e0a0f434f 100644
--- a/pkg/services/alerting/notifier.go
+++ b/pkg/services/alerting/notifier.go
@@ -3,7 +3,6 @@ package alerting
 import (
 	"errors"
 	"fmt"
-	"time"
 
 	"golang.org/x/sync/errgroup"
 
@@ -81,7 +80,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
 	renderOpts := rendering.Opts{
 		Width:   1000,
 		Height:  500,
-		Timeout: time.Second * 30,
+		Timeout: alertTimeout / 2,
 		OrgId:   context.Rule.OrgId,
 		OrgRole: m.ROLE_ADMIN,
 	}

From b8a1385c77fd15ce7a15c3be956334f20f4de339 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 13 Aug 2018 15:38:37 +0200
Subject: [PATCH 302/380] build: increase frontend tests timeout without no
 output

---
 .circleci/config.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8f2e9b6c1af..977121c30ee 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -104,6 +104,7 @@ jobs:
       - run:
           name: yarn install
           command: 'yarn install --pure-lockfile --no-progress'
+          no_output_timeout: 15m
       - save_cache:
           key: dependency-cache-{{ checksum "yarn.lock" }}
           paths:

From b0f3ca16d9acc839560619284613814f4fcb3797 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 13 Aug 2018 15:40:37 +0200
Subject: [PATCH 303/380] Update CHANGELOG.md

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a36943af65..6eea9bb7337 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@
 * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
+* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
 * **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)

From 1c185ef8d824158765ecb2919c772a68876ecc74 Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Mon, 13 Aug 2018 15:40:52 +0200
Subject: [PATCH 304/380] Add commit to external stylesheet url (#12902)

- currently only the release is used as a fingerprint which produces
  caching issues for all lastest master builds
- also add build commit to url fingerprint
- make bra also watch go html template files
---
 .bra.toml                        | 2 +-
 public/views/index.template.html | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/.bra.toml b/.bra.toml
index dcf316466d6..15961e1e3fd 100644
--- a/.bra.toml
+++ b/.bra.toml
@@ -9,7 +9,7 @@ watch_dirs = [
 	"$WORKDIR/public/views",
 	"$WORKDIR/conf",
 ]
-watch_exts = [".go", ".ini", ".toml"]
+watch_exts = [".go", ".ini", ".toml", ".template.html"]
 build_delay = 1500
 cmds = [
   ["go", "run", "build.go", "-dev", "build-server"],
diff --git a/public/views/index.template.html b/public/views/index.template.html
index ae35666b189..f4c5d183fc8 100644
--- a/public/views/index.template.html
+++ b/public/views/index.template.html
@@ -11,7 +11,7 @@
 
   <base href="[[.AppSubUrl]]/" />
 
-  <link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]">
+  <link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]+[[ .BuildCommit ]]">
 
   <link rel="icon" type="image/png" href="public/img/fav32.png">
   <link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
@@ -107,12 +107,12 @@
     <iframe src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]" height="0" width="0" style="display:none;visibility:hidden"></iframe>
   </noscript>
   <script>(function (w, d, s, l, i) {
-    w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0],
-      j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
+      w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0],
+        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
     })(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');</script>
   <!-- End Google Tag Manager -->
   [[end]]
 
 </body>
 
-</html>
+</html>
\ No newline at end of file

From 39669e5002207fd0b486eeb49a0fa417b51a1e09 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 13 Aug 2018 15:41:15 +0200
Subject: [PATCH 305/380] fix redirect to panel when using an outdated
 dashboard slug (#12901)

---
 public/app/routes/dashboard_loaders.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts
index 3642b54c790..b33d5b6afb1 100644
--- a/public/app/routes/dashboard_loaders.ts
+++ b/public/app/routes/dashboard_loaders.ts
@@ -34,7 +34,9 @@ export class LoadDashboardCtrl {
         const url = locationUtil.stripBaseFromUrl(result.meta.url);
 
         if (url !== $location.path()) {
+          // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times.
           $location.path(url).replace();
+          return;
         }
       }
 

From 9031866caaa64b71a38395815985b715e821582e Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Mon, 13 Aug 2018 15:51:19 +0200
Subject: [PATCH 306/380] changelog: add notes about closing #12805

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6eea9bb7337..efc7e44d31b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 * **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
 * **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
 * **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
+* **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi)
 
 ### Minor
 

From 472b880939c98716de1ad5f654bb99e79aa11627 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Mon, 13 Aug 2018 15:51:58 +0200
Subject: [PATCH 307/380] Add React container

---
 .../panel/heatmap/HeatmapRenderContainer.tsx  | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)
 create mode 100644 public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx

diff --git a/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx b/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx
new file mode 100644
index 00000000000..e5982a485ca
--- /dev/null
+++ b/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import HeatmapRenderer from './rendering';
+import { HeatmapCtrl } from './heatmap_ctrl';
+
+export class HeatmapRenderContainer extends React.Component {
+  renderer: any;
+  constructor(props) {
+    super(props);
+    this.renderer = HeatmapRenderer(
+      this.props.scope,
+      this.props.children[0],
+      [],
+      new HeatmapCtrl(this.props.scope, {}, {})
+    );
+  }
+
+  render() {
+    return <div />;
+  }
+}

From c521f51780b12937cdc1c9c844f92d9515190320 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 13 Aug 2018 15:56:11 +0200
Subject: [PATCH 308/380] tech: removed js related stuff now that 99% is
 typescript (#12905)

---
 .jscs.json                      |  13 --
 .jshintrc                       |  37 ----
 Gruntfile.js                    |   1 -
 package.json                    |   3 -
 scripts/grunt/default_task.js   |   4 -
 scripts/grunt/options/jscs.js   |  22 --
 scripts/grunt/options/jshint.js |  20 --
 tasks/options/copy.js           |  45 ----
 yarn.lock                       | 368 +++-----------------------------
 9 files changed, 28 insertions(+), 485 deletions(-)
 delete mode 100644 .jscs.json
 delete mode 100644 .jshintrc
 delete mode 100644 scripts/grunt/options/jscs.js
 delete mode 100644 scripts/grunt/options/jshint.js
 delete mode 100644 tasks/options/copy.js

diff --git a/.jscs.json b/.jscs.json
deleted file mode 100644
index 8fdad332de5..00000000000
--- a/.jscs.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-    "disallowImplicitTypeConversion": ["string"],
-    "disallowKeywords": ["with"],
-    "disallowMultipleLineBreaks": true,
-    "disallowMixedSpacesAndTabs": true,
-    "disallowTrailingWhitespace": true,
-    "requireSpacesInFunctionExpression": {
-        "beforeOpeningCurlyBrace": true
-    },
-    "disallowSpacesInsideArrayBrackets": true,
-    "disallowSpacesInsideParentheses": true,
-    "validateIndentation": 2
-}
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index 1d8fad63173..00000000000
--- a/.jshintrc
+++ /dev/null
@@ -1,37 +0,0 @@
-{
-  "browser": true,
-  "esversion": 6,
-  "bitwise":false,
-  "curly": true,
-  "eqnull": true,
-  "strict": false,
-  "devel": true,
-  "eqeqeq": true,
-  "forin": false,
-  "immed": true,
-  "supernew": true,
-  "expr": true,
-  "indent": 2,
-  "latedef": false,
-  "newcap": true,
-  "noarg": true,
-  "noempty": true,
-  "undef": true,
-  "boss": true,
-  "trailing": true,
-  "laxbreak": true,
-  "laxcomma": true,
-  "sub": true,
-  "unused": true,
-  "maxdepth": 6,
-  "maxlen": 140,
-
-  "globals": {
-    "System": true,
-    "Promise": true,
-    "define": true,
-    "require": true,
-    "Chromath": false,
-    "setImmediate": true
-  }
-}
diff --git a/Gruntfile.js b/Gruntfile.js
index 23276e8a122..8a71fb44148 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,4 +1,3 @@
-/* jshint node:true */
 'use strict';
 module.exports = function (grunt) {
   var os = require('os');
diff --git a/package.json b/package.json
index 200285d7a1e..24e23b574df 100644
--- a/package.json
+++ b/package.json
@@ -45,9 +45,7 @@
     "grunt-contrib-concat": "^1.0.1",
     "grunt-contrib-copy": "~1.0.0",
     "grunt-contrib-cssmin": "~1.0.2",
-    "grunt-contrib-jshint": "~1.1.0",
     "grunt-exec": "^1.0.1",
-    "grunt-jscs": "3.0.1",
     "grunt-karma": "~2.0.0",
     "grunt-notify": "^0.4.5",
     "grunt-postcss": "^0.8.0",
@@ -60,7 +58,6 @@
     "html-webpack-plugin": "^3.2.0",
     "husky": "^0.14.3",
     "jest": "^22.0.4",
-    "jshint-stylish": "~2.2.1",
     "karma": "1.7.0",
     "karma-chrome-launcher": "~2.2.0",
     "karma-expect": "~1.1.3",
diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js
index 719f0ab4e95..efcdcd02963 100644
--- a/scripts/grunt/default_task.js
+++ b/scripts/grunt/default_task.js
@@ -9,8 +9,6 @@ module.exports = function(grunt) {
   ]);
 
   grunt.registerTask('test', [
-    'jscs',
-    'jshint',
     'sasslint',
     'exec:tslint',
     "exec:jest",
@@ -19,8 +17,6 @@ module.exports = function(grunt) {
   ]);
 
   grunt.registerTask('precommit', [
-    'jscs',
-    'jshint',
     'sasslint',
     'exec:tslint',
     'no-only-tests'
diff --git a/scripts/grunt/options/jscs.js b/scripts/grunt/options/jscs.js
deleted file mode 100644
index 8296e59a506..00000000000
--- a/scripts/grunt/options/jscs.js
+++ /dev/null
@@ -1,22 +0,0 @@
-module.exports = function(config) {
-  return {
-    src: [
-      'Gruntfile.js',
-      '<%= srcDir %>/app/**/*.js',
-      '<%= srcDir %>/plugin/**/*.js',
-      '!<%= srcDir %>/app/dashboards/*'
-    ],
-    options: {
-      config: ".jscs.json",
-    },
-  };
-};
-
-/*
- "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"],
-    "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"],
-    "disallowLeftStickedOperators": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
-    "disallowRightStickedOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
-    "requireRightStickedOperators": ["!"],
-    "requireLeftStickedOperators": [","],
-   */
diff --git a/scripts/grunt/options/jshint.js b/scripts/grunt/options/jshint.js
deleted file mode 100644
index 7ea36eac3ff..00000000000
--- a/scripts/grunt/options/jshint.js
+++ /dev/null
@@ -1,20 +0,0 @@
-module.exports = function(config) {
-  return {
-    source: {
-      files: {
-        src: ['Gruntfile.js', '<%= srcDir %>/app/**/*.js'],
-      }
-    },
-    options: {
-      jshintrc: true,
-      reporter: require('jshint-stylish'),
-      ignores: [
-        'node_modules/*',
-        'dist/*',
-        'sample/*',
-        '<%= srcDir %>/vendor/*',
-        '<%= srcDir %>/app/dashboards/*'
-      ]
-    }
-  };
-};
diff --git a/tasks/options/copy.js b/tasks/options/copy.js
deleted file mode 100644
index 1ef32af6951..00000000000
--- a/tasks/options/copy.js
+++ /dev/null
@@ -1,45 +0,0 @@
-module.exports = function(config) {
-  return {
-    // copy source to temp, we will minify in place for the dist build
-    everything_but_less_to_temp: {
-      cwd: '<%= srcDir %>',
-      expand: true,
-      src: ['**/*', '!**/*.less'],
-      dest: '<%= tempDir %>'
-    },
-
-    public_to_gen: {
-      cwd: '<%= srcDir %>',
-      expand: true,
-      src: ['**/*', '!**/*.less'],
-      dest: '<%= genDir %>'
-    },
-
-    node_modules: {
-      cwd: './node_modules',
-      expand: true,
-      src: [
-        'ace-builds/src-noconflict/**/*',
-        'eventemitter3/*.js',
-        'systemjs/dist/*.js',
-        'es6-promise/**/*',
-        'es6-shim/*.js',
-        'reflect-metadata/*.js',
-        'reflect-metadata/*.ts',
-        'reflect-metadata/*.d.ts',
-        'rxjs/**/*',
-        'tether/**/*',
-        'tether-drop/**/*',
-        'tether-drop/**/*',
-        'remarkable/dist/*',
-        'remarkable/dist/*',
-        'virtual-scroll/**/*',
-        'mousetrap/**/*',
-        'twemoji/2/twemoji.amd*',
-        'twemoji/2/svg/*.svg',
-      ],
-      dest: '<%= srcDir %>/vendor/npm'
-    }
-
-  };
-};
diff --git a/yarn.lock b/yarn.lock
index ed8a1eabec3..89e74828351 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -414,10 +414,6 @@ JSONStream@^1.3.2:
     jsonparse "^1.2.0"
     through ">=2.2.7 <3"
 
-JSV@^4.0.x:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
-
 abab@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
@@ -869,10 +865,6 @@ async-limiter@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
 
-async@0.2.x, async@~0.2.6, async@~0.2.9:
-  version "0.2.10"
-  resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-
 async@^1.4.0, async@^1.5.0, async@^1.5.2, async@~1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@@ -883,6 +875,10 @@ async@^2.0.0, async@^2.1.4, async@^2.4.1, async@^2.6.0:
   dependencies:
     lodash "^4.17.10"
 
+async@~0.2.6:
+  version "0.2.10"
+  resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
+
 asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -1564,7 +1560,7 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26
     lodash "^4.17.4"
     to-fast-properties "^1.0.3"
 
-babylon@^6.17.3, babylon@^6.18.0, babylon@^6.8.1:
+babylon@^6.17.3, babylon@^6.18.0:
   version "6.18.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
 
@@ -1626,10 +1622,6 @@ bcrypt-pbkdf@^1.0.0:
   dependencies:
     tweetnacl "^0.14.3"
 
-beeper@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809"
-
 better-assert@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
@@ -2109,7 +2101,7 @@ center-align@^0.1.1:
     align-text "^0.1.3"
     lazy-cache "^1.0.3"
 
-chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1:
+chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -2315,7 +2307,7 @@ cli-table2@^0.2.0, cli-table2@~0.2.0:
   optionalDependencies:
     colors "^1.1.2"
 
-cli-table@^0.3.1, cli-table@~0.3.1:
+cli-table@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
   dependencies:
@@ -2332,13 +2324,6 @@ cli-width@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
 
-cli@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14"
-  dependencies:
-    exit "0.1.2"
-    glob "^7.1.1"
-
 clipboard@^1.7.1:
   version "1.7.1"
   resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
@@ -2490,10 +2475,6 @@ colors@0.5.x:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774"
 
-colors@0.6.x:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
-
 colors@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
@@ -2539,7 +2520,7 @@ commander@2.8.x:
   dependencies:
     graceful-readlink ">= 1.0.0"
 
-commander@2.9.x, commander@~2.9.0:
+commander@2.9.x:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
   dependencies:
@@ -2549,12 +2530,6 @@ commander@~2.13.0:
   version "2.13.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
 
-comment-parser@^0.3.1:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.3.2.tgz#3c03f0776b86a36dfd9a0a2c97c6307f332082fe"
-  dependencies:
-    readable-stream "^2.0.4"
-
 commondir@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -2660,7 +2635,7 @@ connect@^3.6.0:
     parseurl "~1.3.2"
     utils-merge "1.0.1"
 
-console-browserify@1.1.x, console-browserify@^1.1.0:
+console-browserify@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
   dependencies:
@@ -2978,14 +2953,6 @@ csstype@^2.2.0:
   version "2.5.3"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7"
 
-cst@^0.4.3:
-  version "0.4.10"
-  resolved "https://registry.yarnpkg.com/cst/-/cst-0.4.10.tgz#9c05c825290a762f0a85c0aabb8c0fe035ae8516"
-  dependencies:
-    babel-runtime "^6.9.2"
-    babylon "^6.8.1"
-    source-map-support "^0.4.0"
-
 currently-unhandled@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -2996,10 +2963,6 @@ custom-event@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
 
-cycle@1.0.x:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
-
 cyclist@~0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@@ -3324,7 +3287,7 @@ dedent@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
 
-deep-equal@*, deep-equal@^1.0.1:
+deep-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
 
@@ -3604,12 +3567,6 @@ domhandler@2.1:
   dependencies:
     domelementtype "1"
 
-domhandler@2.3:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
-  dependencies:
-    domelementtype "1"
-
 domhandler@^2.3.0:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
@@ -3622,7 +3579,7 @@ domutils@1.1:
   dependencies:
     domelementtype "1"
 
-domutils@1.5, domutils@1.5.1:
+domutils@1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
   dependencies:
@@ -3813,10 +3770,6 @@ ent@~2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
 
-entities@1.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26"
-
 entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
@@ -4181,7 +4134,7 @@ exit-hook@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
 
-exit@0.1.2, exit@0.1.x, exit@^0.1.2, exit@~0.1.1, exit@~0.1.2:
+exit@^0.1.2, exit@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
 
@@ -4362,10 +4315,6 @@ extsprintf@^1.2.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
 
-eyes@0.1.x:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
-
 fast-deep-equal@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@@ -4945,9 +4894,9 @@ glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glo
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^5.0.1, glob@~5.0.0:
-  version "5.0.15"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
+glob@^6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
   dependencies:
     inflight "^1.0.4"
     inherits "2"
@@ -4955,9 +4904,9 @@ glob@^5.0.1, glob@~5.0.0:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^6.0.4:
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+glob@~5.0.0:
+  version "5.0.15"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
   dependencies:
     inflight "^1.0.4"
     inherits "2"
@@ -5199,27 +5148,10 @@ grunt-contrib-cssmin@~1.0.2:
     clean-css "~3.4.2"
     maxmin "^1.1.0"
 
-grunt-contrib-jshint@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/grunt-contrib-jshint/-/grunt-contrib-jshint-1.1.0.tgz#369d909b2593c40e8be79940b21340850c7939ac"
-  dependencies:
-    chalk "^1.1.1"
-    hooker "^0.2.3"
-    jshint "~2.9.4"
-
 grunt-exec@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/grunt-exec/-/grunt-exec-1.0.1.tgz#e5d53a39c5f346901305edee5c87db0f2af999c4"
 
-grunt-jscs@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/grunt-jscs/-/grunt-jscs-3.0.1.tgz#1fae50e3e955df9e3a9d9425aec22accae008092"
-  dependencies:
-    hooker "~0.2.3"
-    jscs "~3.0.5"
-    lodash "~4.6.1"
-    vow "~0.4.1"
-
 grunt-karma@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/grunt-karma/-/grunt-karma-2.0.0.tgz#753583d115dfdc055fe57e58f96d6b3c7e612118"
@@ -5530,7 +5462,7 @@ homedir-polyfill@^1.0.1:
   dependencies:
     parse-passwd "^1.0.0"
 
-hooker@^0.2.3, hooker@~0.2.3:
+hooker@~0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959"
 
@@ -5614,16 +5546,6 @@ html-webpack-plugin@^3.2.0:
     toposort "^1.0.0"
     util.promisify "1.0.0"
 
-htmlparser2@3.8.3, htmlparser2@3.8.x:
-  version "3.8.3"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
-  dependencies:
-    domelementtype "1"
-    domhandler "2.3"
-    domutils "1.5"
-    entities "1.0"
-    readable-stream "1.1"
-
 htmlparser2@^3.9.1:
   version "3.9.2"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -5739,10 +5661,6 @@ husky@^0.14.3:
     normalize-path "^1.0.0"
     strip-indent "^2.0.0"
 
-i@0.3.x:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/i/-/i-0.3.6.tgz#d96c92732076f072711b6b10fd7d4f65ad8ee23d"
-
 iconv-lite@0.4, iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
   version "0.4.23"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
@@ -5838,10 +5756,6 @@ inflight@^1.0.4, inflight@~1.0.6:
     once "^1.3.0"
     wrappy "1"
 
-inherit@^2.2.2:
-  version "2.2.6"
-  resolved "https://registry.yarnpkg.com/inherit/-/inherit-2.2.6.tgz#f1614b06c8544e8128e4229c86347db73ad9788d"
-
 inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@@ -5938,10 +5852,6 @@ ipaddr.js@1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b"
 
-irregular-plurals@^1.0.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.4.0.tgz#2ca9b033651111855412f16be5d77c62a458a766"
-
 is-absolute-url@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@@ -6348,7 +6258,7 @@ isomorphic-fetch@^2.1.1:
     node-fetch "^1.0.1"
     whatwg-fetch ">=0.10.0"
 
-isstream@0.1.x, isstream@~0.1.2:
+isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 
@@ -6748,14 +6658,6 @@ js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0,
     argparse "^1.0.7"
     esprima "^4.0.0"
 
-js-yaml@~3.4.0:
-  version "3.4.6"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.4.6.tgz#6be1b23f6249f53d293370fd4d1aaa63ce1b4eb0"
-  dependencies:
-    argparse "^1.0.2"
-    esprima "^2.6.0"
-    inherit "^2.2.2"
-
 js-yaml@~3.5.2:
   version "3.5.5"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.5.5.tgz#0377c38017cabc7322b0d1fbcd25a491641f2fbe"
@@ -6814,54 +6716,6 @@ jscodeshift@^0.5.0:
     temp "^0.8.1"
     write-file-atomic "^1.2.0"
 
-jscs-jsdoc@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/jscs-jsdoc/-/jscs-jsdoc-2.0.0.tgz#f53ebce029aa3125bd88290ba50d64d4510a4871"
-  dependencies:
-    comment-parser "^0.3.1"
-    jsdoctypeparser "~1.2.0"
-
-jscs-preset-wikimedia@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/jscs-preset-wikimedia/-/jscs-preset-wikimedia-1.0.1.tgz#a6a5fa5967fd67a5d609038e1c794eaf41d4233d"
-
-jscs@~3.0.5:
-  version "3.0.7"
-  resolved "https://registry.yarnpkg.com/jscs/-/jscs-3.0.7.tgz#7141b4dff5b86e32d0e99d764b836767c30d201a"
-  dependencies:
-    chalk "~1.1.0"
-    cli-table "~0.3.1"
-    commander "~2.9.0"
-    cst "^0.4.3"
-    estraverse "^4.1.0"
-    exit "~0.1.2"
-    glob "^5.0.1"
-    htmlparser2 "3.8.3"
-    js-yaml "~3.4.0"
-    jscs-jsdoc "^2.0.0"
-    jscs-preset-wikimedia "~1.0.0"
-    jsonlint "~1.6.2"
-    lodash "~3.10.0"
-    minimatch "~3.0.0"
-    natural-compare "~1.2.2"
-    pathval "~0.1.1"
-    prompt "~0.2.14"
-    reserved-words "^0.1.1"
-    resolve "^1.1.6"
-    strip-bom "^2.0.0"
-    strip-json-comments "~1.0.2"
-    to-double-quotes "^2.0.0"
-    to-single-quotes "^2.0.0"
-    vow "~0.4.8"
-    vow-fs "~0.3.4"
-    xmlbuilder "^3.1.0"
-
-jsdoctypeparser@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-1.2.0.tgz#e7dedc153a11849ffc5141144ae86a7ef0c25392"
-  dependencies:
-    lodash "^3.7.0"
-
 jsdom@^11.5.1:
   version "11.11.0"
   resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.11.0.tgz#df486efad41aee96c59ad7a190e2449c7eb1110e"
@@ -6901,30 +6755,6 @@ jsesc@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
 
-jshint-stylish@~2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/jshint-stylish/-/jshint-stylish-2.2.1.tgz#242082a2c035ae03fd81044e0570cc4208cf6e61"
-  dependencies:
-    beeper "^1.1.0"
-    chalk "^1.0.0"
-    log-symbols "^1.0.0"
-    plur "^2.1.0"
-    string-length "^1.0.0"
-    text-table "^0.2.0"
-
-jshint@~2.9.4:
-  version "2.9.5"
-  resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.9.5.tgz#1e7252915ce681b40827ee14248c46d34e9aa62c"
-  dependencies:
-    cli "~1.0.0"
-    console-browserify "1.1.x"
-    exit "0.1.x"
-    htmlparser2 "3.8.x"
-    lodash "3.7.x"
-    minimatch "~3.0.2"
-    shelljs "0.3.x"
-    strip-json-comments "1.0.x"
-
 json-buffer@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
@@ -6981,13 +6811,6 @@ jsonify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
 
-jsonlint@~1.6.2:
-  version "1.6.3"
-  resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.3.tgz#cb5e31efc0b78291d0d862fbef05900adf212988"
-  dependencies:
-    JSV "^4.0.x"
-    nomnom "^1.5.x"
-
 jsonparse@^1.2.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
@@ -7497,11 +7320,7 @@ lodash.without@~4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
 
-lodash@3.7.x:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.7.0.tgz#3678bd8ab995057c07ade836ed2ef087da811d45"
-
-lodash@^3.10.1, lodash@^3.5.0, lodash@^3.6.0, lodash@^3.7.0, lodash@^3.8.0, lodash@~3.10.0:
+lodash@^3.10.1, lodash@^3.6.0, lodash@^3.8.0:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
@@ -7513,11 +7332,7 @@ lodash@~4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.3.0.tgz#efd9c4a6ec53f3b05412429915c3e4824e4d25a4"
 
-lodash@~4.6.1:
-  version "4.6.1"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.6.1.tgz#df00c1164ad236b183cfc3887a5e8d38cc63cbbc"
-
-log-symbols@^1.0.0, log-symbols@^1.0.2:
+log-symbols@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
   dependencies:
@@ -7990,7 +7805,7 @@ mixin-object@^2.0.1:
     for-in "^0.1.3"
     is-extendable "^0.1.1"
 
-mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -8121,20 +7936,12 @@ natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
 
-natural-compare@~1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.2.2.tgz#1f96d60e3141cac1b6d05653ce0daeac763af6aa"
-
 ncname@1.0.x:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c"
   dependencies:
     xml-char-classes "^1.0.0"
 
-ncp@0.4.x:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/ncp/-/ncp-0.4.2.tgz#abcc6cbd3ec2ed2a729ff6e7c1fa8f01784a8574"
-
 nearley@^2.7.10:
   version "2.13.0"
   resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.13.0.tgz#6e7b0f4e68bfc3e74c99eaef2eda39e513143439"
@@ -8359,7 +8166,7 @@ node-sass@^4.7.2:
     stdout-stream "^1.4.0"
     "true-case-path" "^1.0.2"
 
-nomnom@^1.5.x, nomnom@^1.8.1:
+nomnom@^1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
   dependencies:
@@ -9207,10 +9014,6 @@ path-type@^3.0.0:
   dependencies:
     pify "^3.0.0"
 
-pathval@~0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/pathval/-/pathval-0.1.1.tgz#08f911cdca9cce5942880da7817bc0b723b66d82"
-
 pbkdf2@^3.0.3:
   version "3.0.16"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"
@@ -9273,20 +9076,6 @@ pkg-up@^1.0.0:
   dependencies:
     find-up "^1.0.0"
 
-pkginfo@0.3.x:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
-
-pkginfo@0.x.x:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
-
-plur@^2.1.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a"
-  dependencies:
-    irregular-plurals "^1.0.0"
-
 pluralize@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
@@ -9810,16 +9599,6 @@ promise@^7.1.1:
   dependencies:
     asap "~2.0.3"
 
-prompt@~0.2.14:
-  version "0.2.14"
-  resolved "https://registry.yarnpkg.com/prompt/-/prompt-0.2.14.tgz#57754f64f543fd7b0845707c818ece618f05ffdc"
-  dependencies:
-    pkginfo "0.x.x"
-    read "1.0.x"
-    revalidator "0.1.x"
-    utile "0.2.x"
-    winston "0.8.x"
-
 promzard@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee"
@@ -10304,7 +10083,7 @@ read-pkg@^3.0.0:
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
 
-read@1, read@1.0.x, read@~1.0.1, read@~1.0.7:
+read@1, read@~1.0.1, read@~1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
   dependencies:
@@ -10331,15 +10110,6 @@ readable-stream@1.0, readable-stream@~1.0.2:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@1.1:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
 readable-stream@~1.1.10:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -10660,10 +10430,6 @@ requires-port@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
 
-reserved-words@^0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
-
 resolve-cwd@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -10754,17 +10520,13 @@ retry@^0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
 
-revalidator@0.1.x:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b"
-
 right-align@^0.1.1:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
   dependencies:
     align-text "^0.1.1"
 
-rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -11116,10 +10878,6 @@ shell-quote@^1.6.1:
     array-reduce "~0.0.0"
     jsonify "~0.0.0"
 
-shelljs@0.3.x:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1"
-
 shelljs@^0.6.0:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8"
@@ -11432,7 +11190,7 @@ source-map-resolve@^0.5.0:
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
-source-map-support@^0.4.0, source-map-support@^0.4.15:
+source-map-support@^0.4.15:
   version "0.4.18"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
   dependencies:
@@ -11555,10 +11313,6 @@ stack-parser@^0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/stack-parser/-/stack-parser-0.0.1.tgz#7d3b63a17887e9e2c2bf55dbd3318fe34a39d1e7"
 
-stack-trace@0.0.x:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
-
 stack-utils@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
@@ -11649,12 +11403,6 @@ strict-uri-encode@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
 
-string-length@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac"
-  dependencies:
-    strip-ansi "^3.0.0"
-
 string-length@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
@@ -11766,7 +11514,7 @@ strip-indent@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
 
-strip-json-comments@1.0.x, strip-json-comments@~1.0.1, strip-json-comments@~1.0.2:
+strip-json-comments@~1.0.1:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
 
@@ -12029,10 +11777,6 @@ to-buffer@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
 
-to-double-quotes@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/to-double-quotes/-/to-double-quotes-2.0.0.tgz#aaf231d6fa948949f819301bbab4484d8588e4a7"
-
 to-fast-properties@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
@@ -12059,10 +11803,6 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     regex-not "^1.0.2"
     safe-regex "^1.1.0"
 
-to-single-quotes@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/to-single-quotes/-/to-single-quotes-2.0.1.tgz#7cc29151f0f5f2c41946f119f5932fe554170125"
-
 toposort@^1.0.0:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
@@ -12527,25 +12267,10 @@ utila@~0.4:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
 
-utile@0.2.x:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/utile/-/utile-0.2.1.tgz#930c88e99098d6220834c356cbd9a770522d90d7"
-  dependencies:
-    async "~0.2.9"
-    deep-equal "*"
-    i "0.3.x"
-    mkdirp "0.x.x"
-    ncp "0.4.x"
-    rimraf "2.x.x"
-
 utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
 
-uuid@^2.0.2:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
-
 uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
@@ -12623,25 +12348,6 @@ void-elements@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
 
-vow-fs@~0.3.4:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/vow-fs/-/vow-fs-0.3.6.tgz#2d4c59be22e2bf2618ddf597ab4baa923be7200d"
-  dependencies:
-    glob "^7.0.5"
-    uuid "^2.0.2"
-    vow "^0.4.7"
-    vow-queue "^0.4.1"
-
-vow-queue@^0.4.1:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/vow-queue/-/vow-queue-0.4.3.tgz#4ba8f64b56e9212c0dbe57f1405aeebd54cce78d"
-  dependencies:
-    vow "^0.4.17"
-
-vow@^0.4.17, vow@^0.4.7, vow@~0.4.1, vow@~0.4.8:
-  version "0.4.17"
-  resolved "https://registry.yarnpkg.com/vow/-/vow-0.4.17.tgz#b16e08fae58c52f3ebc6875f2441b26a92682904"
-
 vue-parser@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/vue-parser/-/vue-parser-1.1.6.tgz#3063c8431795664ebe429c23b5506899706e6355"
@@ -12960,18 +12666,6 @@ window-size@0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
 
-winston@0.8.x:
-  version "0.8.3"
-  resolved "https://registry.yarnpkg.com/winston/-/winston-0.8.3.tgz#64b6abf4cd01adcaefd5009393b1d8e8bec19db0"
-  dependencies:
-    async "0.2.x"
-    colors "0.6.x"
-    cycle "1.0.x"
-    eyes "0.1.x"
-    isstream "0.1.x"
-    pkginfo "0.3.x"
-    stack-trace "0.0.x"
-
 wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
@@ -13053,12 +12747,6 @@ xml-name-validator@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
 
-xmlbuilder@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-3.1.0.tgz#2c86888f2d4eade850fa38ca7f7223f7209516e1"
-  dependencies:
-    lodash "^3.5.0"
-
 xmlhttprequest-ssl@1.5.3:
   version "1.5.3"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"

From 739bee020779fa9af6ac88f087b33cc1d37328df Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <dehrax@users.noreply.github.com>
Date: Mon, 13 Aug 2018 16:08:01 +0200
Subject: [PATCH 309/380] Karma to Jest: graph (refactor) (#12860)

* Begin conversion

* Test setup started

* Begin rewrite of graph

* Rewrite as class

* Some tests passing

* Fix binding errors

* Half tests passing

* Call buildFlotPairs. More tests passing

* All tests passing

* Remove test test

* Remove Karma test

* Make methods out of event functions

* Rename GraphElement
---
 public/app/plugins/panel/graph/graph.ts       | 1403 +++++++++--------
 public/app/plugins/panel/graph/module.ts      |    1 +
 .../plugins/panel/graph/specs/graph.jest.ts   |  518 ++++++
 .../plugins/panel/graph/specs/graph_specs.ts  |  454 ------
 4 files changed, 1236 insertions(+), 1140 deletions(-)
 create mode 100644 public/app/plugins/panel/graph/specs/graph.jest.ts
 delete mode 100644 public/app/plugins/panel/graph/specs/graph_specs.ts

diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts
index 9f216c12288..35886aa5bf7 100755
--- a/public/app/plugins/panel/graph/graph.ts
+++ b/public/app/plugins/panel/graph/graph.ts
@@ -21,699 +21,730 @@ import { convertToHistogramData } from './histogram';
 import { alignYLevel } from './align_yaxes';
 import config from 'app/core/config';
 
+import { GraphCtrl } from './module';
+
+class GraphElement {
+  ctrl: GraphCtrl;
+  tooltip: any;
+  dashboard: any;
+  annotations: Array<object>;
+  panel: any;
+  plot: any;
+  sortedSeries: Array<any>;
+  data: Array<any>;
+  panelWidth: number;
+  eventManager: EventManager;
+  thresholdManager: ThresholdManager;
+
+  constructor(private scope, private elem, private timeSrv) {
+    this.ctrl = scope.ctrl;
+    this.dashboard = this.ctrl.dashboard;
+    this.panel = this.ctrl.panel;
+    this.annotations = [];
+
+    this.panelWidth = 0;
+    this.eventManager = new EventManager(this.ctrl);
+    this.thresholdManager = new ThresholdManager(this.ctrl);
+    this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {
+      return this.sortedSeries;
+    });
+
+    // panel events
+    this.ctrl.events.on('panel-teardown', this.onPanelteardown.bind(this));
+
+    /**
+     * Split graph rendering into two parts.
+     * First, calculate series stats in buildFlotPairs() function. Then legend rendering started
+     * (see ctrl.events.on('render') in legend.ts).
+     * When legend is rendered it emits 'legend-rendering-complete' and graph rendered.
+     */
+    this.ctrl.events.on('render', this.onRender.bind(this));
+    this.ctrl.events.on('legend-rendering-complete', this.onLegendRenderingComplete.bind(this));
+
+    // global events
+    appEvents.on('graph-hover', this.onGraphHover.bind(this), scope);
+
+    appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), scope);
+
+    this.elem.bind('plotselected', this.onPlotSelected.bind(this));
+
+    this.elem.bind('plotclick', this.onPlotClick.bind(this));
+    scope.$on('$destroy', this.onScopeDestroy.bind(this));
+  }
+
+  onRender(renderData) {
+    this.data = renderData || this.data;
+    if (!this.data) {
+      return;
+    }
+    this.annotations = this.ctrl.annotations || [];
+    this.buildFlotPairs(this.data);
+    const graphHeight = this.elem.height();
+    updateLegendValues(this.data, this.panel, graphHeight);
+
+    this.ctrl.events.emit('render-legend');
+  }
+
+  onGraphHover(evt) {
+    // ignore other graph hover events if shared tooltip is disabled
+    if (!this.dashboard.sharedTooltipModeEnabled()) {
+      return;
+    }
+
+    // ignore if we are the emitter
+    if (!this.plot || evt.panel.id === this.panel.id || this.ctrl.otherPanelInFullscreenMode()) {
+      return;
+    }
+
+    this.tooltip.show(evt.pos);
+  }
+
+  onPanelteardown() {
+    this.thresholdManager = null;
+
+    if (this.plot) {
+      this.plot.destroy();
+      this.plot = null;
+    }
+  }
+
+  onLegendRenderingComplete() {
+    this.render_panel();
+  }
+
+  onGraphHoverClear(event, info) {
+    if (this.plot) {
+      this.tooltip.clear(this.plot);
+    }
+  }
+
+  onPlotSelected(event, ranges) {
+    if (this.panel.xaxis.mode !== 'time') {
+      // Skip if panel in histogram or series mode
+      this.plot.clearSelection();
+      return;
+    }
+
+    if ((ranges.ctrlKey || ranges.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) {
+      // Add annotation
+      setTimeout(() => {
+        this.eventManager.updateTime(ranges.xaxis);
+      }, 100);
+    } else {
+      this.scope.$apply(() => {
+        this.timeSrv.setTime({
+          from: moment.utc(ranges.xaxis.from),
+          to: moment.utc(ranges.xaxis.to),
+        });
+      });
+    }
+  }
+
+  onPlotClick(event, pos, item) {
+    if (this.panel.xaxis.mode !== 'time') {
+      // Skip if panel in histogram or series mode
+      return;
+    }
+
+    if ((pos.ctrlKey || pos.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) {
+      // Skip if range selected (added in "plotselected" event handler)
+      let isRangeSelection = pos.x !== pos.x1;
+      if (!isRangeSelection) {
+        setTimeout(() => {
+          this.eventManager.updateTime({ from: pos.x, to: null });
+        }, 100);
+      }
+    }
+  }
+
+  onScopeDestroy() {
+    this.tooltip.destroy();
+    this.elem.off();
+    this.elem.remove();
+  }
+
+  shouldAbortRender() {
+    if (!this.data) {
+      return true;
+    }
+
+    if (this.panelWidth === 0) {
+      return true;
+    }
+
+    return false;
+  }
+
+  drawHook(plot) {
+    // add left axis labels
+    if (this.panel.yaxes[0].label && this.panel.yaxes[0].show) {
+      $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
+        .text(this.panel.yaxes[0].label)
+        .appendTo(this.elem);
+    }
+
+    // add right axis labels
+    if (this.panel.yaxes[1].label && this.panel.yaxes[1].show) {
+      $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
+        .text(this.panel.yaxes[1].label)
+        .appendTo(this.elem);
+    }
+
+    if (this.ctrl.dataWarning) {
+      $(`<div class="datapoints-warning flot-temp-elem">${this.ctrl.dataWarning.title}</div>`).appendTo(this.elem);
+    }
+
+    this.thresholdManager.draw(plot);
+  }
+
+  processOffsetHook(plot, gridMargin) {
+    var left = this.panel.yaxes[0];
+    var right = this.panel.yaxes[1];
+    if (left.show && left.label) {
+      gridMargin.left = 20;
+    }
+    if (right.show && right.label) {
+      gridMargin.right = 20;
+    }
+
+    // apply y-axis min/max options
+    var yaxis = plot.getYAxes();
+    for (var i = 0; i < yaxis.length; i++) {
+      var axis = yaxis[i];
+      var panelOptions = this.panel.yaxes[i];
+      axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max;
+      axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min;
+    }
+  }
+
+  processRangeHook(plot) {
+    var yAxes = plot.getYAxes();
+    const align = this.panel.yaxis.align || false;
+
+    if (yAxes.length > 1 && align === true) {
+      const level = this.panel.yaxis.alignLevel || 0;
+      alignYLevel(yAxes, parseFloat(level));
+    }
+  }
+
+  // Series could have different timeSteps,
+  // let's find the smallest one so that bars are correctly rendered.
+  // In addition, only take series which are rendered as bars for this.
+  getMinTimeStepOfSeries(data) {
+    var min = Number.MAX_VALUE;
+
+    for (let i = 0; i < data.length; i++) {
+      if (!data[i].stats.timeStep) {
+        continue;
+      }
+      if (this.panel.bars) {
+        if (data[i].bars && data[i].bars.show === false) {
+          continue;
+        }
+      } else {
+        if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) {
+          continue;
+        }
+      }
+
+      if (data[i].stats.timeStep < min) {
+        min = data[i].stats.timeStep;
+      }
+    }
+
+    return min;
+  }
+
+  // Function for rendering panel
+  render_panel() {
+    this.panelWidth = this.elem.width();
+    if (this.shouldAbortRender()) {
+      return;
+    }
+
+    // give space to alert editing
+    this.thresholdManager.prepare(this.elem, this.data);
+
+    // un-check dashes if lines are unchecked
+    this.panel.dashes = this.panel.lines ? this.panel.dashes : false;
+
+    // Populate element
+    let options: any = this.buildFlotOptions(this.panel);
+    this.prepareXAxis(options, this.panel);
+    this.configureYAxisOptions(this.data, options);
+    this.thresholdManager.addFlotOptions(options, this.panel);
+    this.eventManager.addFlotEvents(this.annotations, options);
+
+    this.sortedSeries = this.sortSeries(this.data, this.panel);
+    this.callPlot(options, true);
+  }
+
+  buildFlotPairs(data) {
+    for (let i = 0; i < data.length; i++) {
+      let series = data[i];
+      series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode);
+
+      // if hidden remove points and disable stack
+      if (this.ctrl.hiddenSeries[series.alias]) {
+        series.data = [];
+        series.stack = false;
+      }
+    }
+  }
+
+  prepareXAxis(options, panel) {
+    switch (panel.xaxis.mode) {
+      case 'series': {
+        options.series.bars.barWidth = 0.7;
+        options.series.bars.align = 'center';
+
+        for (let i = 0; i < this.data.length; i++) {
+          let series = this.data[i];
+          series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
+        }
+
+        this.addXSeriesAxis(options);
+        break;
+      }
+      case 'histogram': {
+        let bucketSize: number;
+
+        if (this.data.length) {
+          let histMin = _.min(_.map(this.data, s => s.stats.min));
+          let histMax = _.max(_.map(this.data, s => s.stats.max));
+          let ticks = panel.xaxis.buckets || this.panelWidth / 50;
+          bucketSize = tickStep(histMin, histMax, ticks);
+          options.series.bars.barWidth = bucketSize * 0.8;
+          this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);
+        } else {
+          bucketSize = 0;
+        }
+
+        this.addXHistogramAxis(options, bucketSize);
+        break;
+      }
+      case 'table': {
+        options.series.bars.barWidth = 0.7;
+        options.series.bars.align = 'center';
+        this.addXTableAxis(options);
+        break;
+      }
+      default: {
+        options.series.bars.barWidth = this.getMinTimeStepOfSeries(this.data) / 1.5;
+        this.addTimeAxis(options);
+        break;
+      }
+    }
+  }
+
+  callPlot(options, incrementRenderCounter) {
+    try {
+      this.plot = $.plot(this.elem, this.sortedSeries, options);
+      if (this.ctrl.renderError) {
+        delete this.ctrl.error;
+        delete this.ctrl.inspector;
+      }
+    } catch (e) {
+      console.log('flotcharts error', e);
+      this.ctrl.error = e.message || 'Render Error';
+      this.ctrl.renderError = true;
+      this.ctrl.inspector = { error: e };
+    }
+
+    if (incrementRenderCounter) {
+      this.ctrl.renderingCompleted();
+    }
+  }
+
+  buildFlotOptions(panel) {
+    let gridColor = '#c8c8c8';
+    if (config.bootData.user.lightTheme === true) {
+      gridColor = '#a1a1a1';
+    }
+    const stack = panel.stack ? true : null;
+    let options = {
+      hooks: {
+        draw: [this.drawHook.bind(this)],
+        processOffset: [this.processOffsetHook.bind(this)],
+        processRange: [this.processRangeHook.bind(this)],
+      },
+      legend: { show: false },
+      series: {
+        stackpercent: panel.stack ? panel.percentage : false,
+        stack: panel.percentage ? null : stack,
+        lines: {
+          show: panel.lines,
+          zero: false,
+          fill: this.translateFillOption(panel.fill),
+          lineWidth: panel.dashes ? 0 : panel.linewidth,
+          steps: panel.steppedLine,
+        },
+        dashes: {
+          show: panel.dashes,
+          lineWidth: panel.linewidth,
+          dashLength: [panel.dashLength, panel.spaceLength],
+        },
+        bars: {
+          show: panel.bars,
+          fill: 1,
+          barWidth: 1,
+          zero: false,
+          lineWidth: 0,
+        },
+        points: {
+          show: panel.points,
+          fill: 1,
+          fillColor: false,
+          radius: panel.points ? panel.pointradius : 2,
+        },
+        shadowSize: 0,
+      },
+      yaxes: [],
+      xaxis: {},
+      grid: {
+        minBorderMargin: 0,
+        markings: [],
+        backgroundColor: null,
+        borderWidth: 0,
+        hoverable: true,
+        clickable: true,
+        color: gridColor,
+        margin: { left: 0, right: 0 },
+        labelMarginX: 0,
+      },
+      selection: {
+        mode: 'x',
+        color: '#666',
+      },
+      crosshair: {
+        mode: 'x',
+      },
+    };
+    return options;
+  }
+
+  sortSeries(series, panel) {
+    var sortBy = panel.legend.sort;
+    var sortOrder = panel.legend.sortDesc;
+    var haveSortBy = sortBy !== null && sortBy !== undefined;
+    var haveSortOrder = sortOrder !== null && sortOrder !== undefined;
+    var shouldSortBy = panel.stack && haveSortBy && haveSortOrder;
+    var sortDesc = panel.legend.sortDesc === true ? -1 : 1;
+
+    if (shouldSortBy) {
+      return _.sortBy(series, s => s.stats[sortBy] * sortDesc);
+    } else {
+      return _.sortBy(series, s => s.zindex);
+    }
+  }
+
+  translateFillOption(fill) {
+    if (this.panel.percentage && this.panel.stack) {
+      return fill === 0 ? 0.001 : fill / 10;
+    } else {
+      return fill / 10;
+    }
+  }
+
+  addTimeAxis(options) {
+    var ticks = this.panelWidth / 100;
+    var min = _.isUndefined(this.ctrl.range.from) ? null : this.ctrl.range.from.valueOf();
+    var max = _.isUndefined(this.ctrl.range.to) ? null : this.ctrl.range.to.valueOf();
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: 'time',
+      min: min,
+      max: max,
+      label: 'Datetime',
+      ticks: ticks,
+      timeformat: this.time_format(ticks, min, max),
+    };
+  }
+
+  addXSeriesAxis(options) {
+    var ticks = _.map(this.data, function(series, index) {
+      return [index + 1, series.alias];
+    });
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: 0,
+      max: ticks.length + 1,
+      label: 'Datetime',
+      ticks: ticks,
+    };
+  }
+
+  addXHistogramAxis(options, bucketSize) {
+    let ticks, min, max;
+    let defaultTicks = this.panelWidth / 50;
+
+    if (this.data.length && bucketSize) {
+      let tick_values = [];
+      for (let d of this.data) {
+        for (let point of d.data) {
+          tick_values[point[0]] = true;
+        }
+      }
+      ticks = Object.keys(tick_values).map(v => Number(v));
+      min = _.min(ticks);
+      max = _.max(ticks);
+
+      // Adjust tick step
+      let tickStep = bucketSize;
+      let ticks_num = Math.floor((max - min) / tickStep);
+      while (ticks_num > defaultTicks) {
+        tickStep = tickStep * 2;
+        ticks_num = Math.ceil((max - min) / tickStep);
+      }
+
+      // Expand ticks for pretty view
+      min = Math.floor(min / tickStep) * tickStep;
+      // 1.01 is 101% - ensure we have enough space for last bar
+      max = Math.ceil(max * 1.01 / tickStep) * tickStep;
+
+      ticks = [];
+      for (let i = min; i <= max; i += tickStep) {
+        ticks.push(i);
+      }
+    } else {
+      // Set defaults if no data
+      ticks = defaultTicks / 2;
+      min = 0;
+      max = 1;
+    }
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: min,
+      max: max,
+      label: 'Histogram',
+      ticks: ticks,
+    };
+
+    // Use 'short' format for histogram values
+    this.configureAxisMode(options.xaxis, 'short');
+  }
+
+  addXTableAxis(options) {
+    var ticks = _.map(this.data, function(series, seriesIndex) {
+      return _.map(series.datapoints, function(point, pointIndex) {
+        var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
+        return [tickIndex + 1, point[1]];
+      });
+    });
+    ticks = _.flatten(ticks, true);
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: 0,
+      max: ticks.length + 1,
+      label: 'Datetime',
+      ticks: ticks,
+    };
+  }
+
+  configureYAxisOptions(data, options) {
+    var defaults = {
+      position: 'left',
+      show: this.panel.yaxes[0].show,
+      index: 1,
+      logBase: this.panel.yaxes[0].logBase || 1,
+      min: this.parseNumber(this.panel.yaxes[0].min),
+      max: this.parseNumber(this.panel.yaxes[0].max),
+      tickDecimals: this.panel.yaxes[0].decimals,
+    };
+
+    options.yaxes.push(defaults);
+
+    if (_.find(data, { yaxis: 2 })) {
+      var secondY = _.clone(defaults);
+      secondY.index = 2;
+      secondY.show = this.panel.yaxes[1].show;
+      secondY.logBase = this.panel.yaxes[1].logBase || 1;
+      secondY.position = 'right';
+      secondY.min = this.parseNumber(this.panel.yaxes[1].min);
+      secondY.max = this.parseNumber(this.panel.yaxes[1].max);
+      secondY.tickDecimals = this.panel.yaxes[1].decimals;
+      options.yaxes.push(secondY);
+
+      this.applyLogScale(options.yaxes[1], data);
+      this.configureAxisMode(
+        options.yaxes[1],
+        this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format
+      );
+    }
+    this.applyLogScale(options.yaxes[0], data);
+    this.configureAxisMode(
+      options.yaxes[0],
+      this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format
+    );
+  }
+
+  parseNumber(value: any) {
+    if (value === null || typeof value === 'undefined') {
+      return null;
+    }
+
+    return _.toNumber(value);
+  }
+
+  applyLogScale(axis, data) {
+    if (axis.logBase === 1) {
+      return;
+    }
+
+    const minSetToZero = axis.min === 0;
+
+    if (axis.min < Number.MIN_VALUE) {
+      axis.min = null;
+    }
+    if (axis.max < Number.MIN_VALUE) {
+      axis.max = null;
+    }
+
+    var series, i;
+    var max = axis.max,
+      min = axis.min;
+
+    for (i = 0; i < data.length; i++) {
+      series = data[i];
+      if (series.yaxis === axis.index) {
+        if (!max || max < series.stats.max) {
+          max = series.stats.max;
+        }
+        if (!min || min > series.stats.logmin) {
+          min = series.stats.logmin;
+        }
+      }
+    }
+
+    axis.transform = function(v) {
+      return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase);
+    };
+    axis.inverseTransform = function(v) {
+      return Math.pow(axis.logBase, v);
+    };
+
+    if (!max && !min) {
+      max = axis.inverseTransform(+2);
+      min = axis.inverseTransform(-2);
+    } else if (!max) {
+      max = min * axis.inverseTransform(+4);
+    } else if (!min) {
+      min = max * axis.inverseTransform(-4);
+    }
+
+    if (axis.min) {
+      min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));
+    } else {
+      min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min)));
+    }
+    if (axis.max) {
+      max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));
+    } else {
+      max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max)));
+    }
+
+    if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {
+      return;
+    }
+
+    if (Number.isFinite(min) && Number.isFinite(max)) {
+      if (minSetToZero) {
+        axis.min = 0.1;
+        min = 1;
+      }
+
+      axis.ticks = this.generateTicksForLogScaleYAxis(min, max, axis.logBase);
+      if (minSetToZero) {
+        axis.ticks.unshift(0.1);
+      }
+      if (axis.ticks[axis.ticks.length - 1] > axis.max) {
+        axis.max = axis.ticks[axis.ticks.length - 1];
+      }
+    } else {
+      axis.ticks = [1, 2];
+      delete axis.min;
+      delete axis.max;
+    }
+  }
+
+  generateTicksForLogScaleYAxis(min, max, logBase) {
+    let ticks = [];
+
+    var nextTick;
+    for (nextTick = min; nextTick <= max; nextTick *= logBase) {
+      ticks.push(nextTick);
+    }
+
+    const maxNumTicks = Math.ceil(this.ctrl.height / 25);
+    const numTicks = ticks.length;
+    if (numTicks > maxNumTicks) {
+      const factor = Math.ceil(numTicks / maxNumTicks) * logBase;
+      ticks = [];
+
+      for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {
+        ticks.push(nextTick);
+      }
+    }
+
+    return ticks;
+  }
+
+  configureAxisMode(axis, format) {
+    axis.tickFormatter = function(val, axis) {
+      if (!kbn.valueFormats[format]) {
+        throw new Error(`Unit '${format}' is not supported`);
+      }
+      return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
+    };
+  }
+
+  time_format(ticks, min, max) {
+    if (min && max && ticks) {
+      var range = max - min;
+      var secPerTick = range / ticks / 1000;
+      var oneDay = 86400000;
+      var oneYear = 31536000000;
+
+      if (secPerTick <= 45) {
+        return '%H:%M:%S';
+      }
+      if (secPerTick <= 7200 || range <= oneDay) {
+        return '%H:%M';
+      }
+      if (secPerTick <= 80000) {
+        return '%m/%d %H:%M';
+      }
+      if (secPerTick <= 2419200 || range <= oneYear) {
+        return '%m/%d';
+      }
+      return '%Y-%m';
+    }
+
+    return '%H:%M';
+  }
+}
+
 /** @ngInject **/
 function graphDirective(timeSrv, popoverSrv, contextSrv) {
   return {
     restrict: 'A',
     template: '',
-    link: function(scope, elem) {
-      var ctrl = scope.ctrl;
-      var dashboard = ctrl.dashboard;
-      var panel = ctrl.panel;
-      var annotations = [];
-      var data;
-      var plot;
-      var sortedSeries;
-      var panelWidth = 0;
-      var eventManager = new EventManager(ctrl);
-      var thresholdManager = new ThresholdManager(ctrl);
-      var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
-        return sortedSeries;
-      });
-
-      // panel events
-      ctrl.events.on('panel-teardown', () => {
-        thresholdManager = null;
-
-        if (plot) {
-          plot.destroy();
-          plot = null;
-        }
-      });
-
-      /**
-       * Split graph rendering into two parts.
-       * First, calculate series stats in buildFlotPairs() function. Then legend rendering started
-       * (see ctrl.events.on('render') in legend.ts).
-       * When legend is rendered it emits 'legend-rendering-complete' and graph rendered.
-       */
-      ctrl.events.on('render', renderData => {
-        data = renderData || data;
-        if (!data) {
-          return;
-        }
-        annotations = ctrl.annotations || [];
-        buildFlotPairs(data);
-        const graphHeight = elem.height();
-        updateLegendValues(data, panel, graphHeight);
-
-        ctrl.events.emit('render-legend');
-      });
-
-      ctrl.events.on('legend-rendering-complete', () => {
-        render_panel();
-      });
-
-      // global events
-      appEvents.on(
-        'graph-hover',
-        evt => {
-          // ignore other graph hover events if shared tooltip is disabled
-          if (!dashboard.sharedTooltipModeEnabled()) {
-            return;
-          }
-
-          // ignore if we are the emitter
-          if (!plot || evt.panel.id === panel.id || ctrl.otherPanelInFullscreenMode()) {
-            return;
-          }
-
-          tooltip.show(evt.pos);
-        },
-        scope
-      );
-
-      appEvents.on(
-        'graph-hover-clear',
-        (event, info) => {
-          if (plot) {
-            tooltip.clear(plot);
-          }
-        },
-        scope
-      );
-
-      function shouldAbortRender() {
-        if (!data) {
-          return true;
-        }
-
-        if (panelWidth === 0) {
-          return true;
-        }
-
-        return false;
-      }
-
-      function drawHook(plot) {
-        // add left axis labels
-        if (panel.yaxes[0].label && panel.yaxes[0].show) {
-          $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
-            .text(panel.yaxes[0].label)
-            .appendTo(elem);
-        }
-
-        // add right axis labels
-        if (panel.yaxes[1].label && panel.yaxes[1].show) {
-          $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
-            .text(panel.yaxes[1].label)
-            .appendTo(elem);
-        }
-
-        if (ctrl.dataWarning) {
-          $(`<div class="datapoints-warning flot-temp-elem">${ctrl.dataWarning.title}</div>`).appendTo(elem);
-        }
-
-        thresholdManager.draw(plot);
-      }
-
-      function processOffsetHook(plot, gridMargin) {
-        var left = panel.yaxes[0];
-        var right = panel.yaxes[1];
-        if (left.show && left.label) {
-          gridMargin.left = 20;
-        }
-        if (right.show && right.label) {
-          gridMargin.right = 20;
-        }
-
-        // apply y-axis min/max options
-        var yaxis = plot.getYAxes();
-        for (var i = 0; i < yaxis.length; i++) {
-          var axis = yaxis[i];
-          var panelOptions = panel.yaxes[i];
-          axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max;
-          axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min;
-        }
-      }
-
-      function processRangeHook(plot) {
-        var yAxes = plot.getYAxes();
-        const align = panel.yaxis.align || false;
-
-        if (yAxes.length > 1 && align === true) {
-          const level = panel.yaxis.alignLevel || 0;
-          alignYLevel(yAxes, parseFloat(level));
-        }
-      }
-
-      // Series could have different timeSteps,
-      // let's find the smallest one so that bars are correctly rendered.
-      // In addition, only take series which are rendered as bars for this.
-      function getMinTimeStepOfSeries(data) {
-        var min = Number.MAX_VALUE;
-
-        for (let i = 0; i < data.length; i++) {
-          if (!data[i].stats.timeStep) {
-            continue;
-          }
-          if (panel.bars) {
-            if (data[i].bars && data[i].bars.show === false) {
-              continue;
-            }
-          } else {
-            if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) {
-              continue;
-            }
-          }
-
-          if (data[i].stats.timeStep < min) {
-            min = data[i].stats.timeStep;
-          }
-        }
-
-        return min;
-      }
-
-      // Function for rendering panel
-      function render_panel() {
-        panelWidth = elem.width();
-        if (shouldAbortRender()) {
-          return;
-        }
-
-        // give space to alert editing
-        thresholdManager.prepare(elem, data);
-
-        // un-check dashes if lines are unchecked
-        panel.dashes = panel.lines ? panel.dashes : false;
-
-        // Populate element
-        let options: any = buildFlotOptions(panel);
-        prepareXAxis(options, panel);
-        configureYAxisOptions(data, options);
-        thresholdManager.addFlotOptions(options, panel);
-        eventManager.addFlotEvents(annotations, options);
-
-        sortedSeries = sortSeries(data, panel);
-        callPlot(options, true);
-      }
-
-      function buildFlotPairs(data) {
-        for (let i = 0; i < data.length; i++) {
-          let series = data[i];
-          series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode);
-
-          // if hidden remove points and disable stack
-          if (ctrl.hiddenSeries[series.alias]) {
-            series.data = [];
-            series.stack = false;
-          }
-        }
-      }
-
-      function prepareXAxis(options, panel) {
-        switch (panel.xaxis.mode) {
-          case 'series': {
-            options.series.bars.barWidth = 0.7;
-            options.series.bars.align = 'center';
-
-            for (let i = 0; i < data.length; i++) {
-              let series = data[i];
-              series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
-            }
-
-            addXSeriesAxis(options);
-            break;
-          }
-          case 'histogram': {
-            let bucketSize: number;
-
-            if (data.length) {
-              let histMin = _.min(_.map(data, s => s.stats.min));
-              let histMax = _.max(_.map(data, s => s.stats.max));
-              let ticks = panel.xaxis.buckets || panelWidth / 50;
-              bucketSize = tickStep(histMin, histMax, ticks);
-              options.series.bars.barWidth = bucketSize * 0.8;
-              data = convertToHistogramData(data, bucketSize, ctrl.hiddenSeries, histMin, histMax);
-            } else {
-              bucketSize = 0;
-            }
-
-            addXHistogramAxis(options, bucketSize);
-            break;
-          }
-          case 'table': {
-            options.series.bars.barWidth = 0.7;
-            options.series.bars.align = 'center';
-            addXTableAxis(options);
-            break;
-          }
-          default: {
-            options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5;
-            addTimeAxis(options);
-            break;
-          }
-        }
-      }
-
-      function callPlot(options, incrementRenderCounter) {
-        try {
-          plot = $.plot(elem, sortedSeries, options);
-          if (ctrl.renderError) {
-            delete ctrl.error;
-            delete ctrl.inspector;
-          }
-        } catch (e) {
-          console.log('flotcharts error', e);
-          ctrl.error = e.message || 'Render Error';
-          ctrl.renderError = true;
-          ctrl.inspector = { error: e };
-        }
-
-        if (incrementRenderCounter) {
-          ctrl.renderingCompleted();
-        }
-      }
-
-      function buildFlotOptions(panel) {
-        let gridColor = '#c8c8c8';
-        if (config.bootData.user.lightTheme === true) {
-          gridColor = '#a1a1a1';
-        }
-        const stack = panel.stack ? true : null;
-        let options = {
-          hooks: {
-            draw: [drawHook],
-            processOffset: [processOffsetHook],
-            processRange: [processRangeHook],
-          },
-          legend: { show: false },
-          series: {
-            stackpercent: panel.stack ? panel.percentage : false,
-            stack: panel.percentage ? null : stack,
-            lines: {
-              show: panel.lines,
-              zero: false,
-              fill: translateFillOption(panel.fill),
-              lineWidth: panel.dashes ? 0 : panel.linewidth,
-              steps: panel.steppedLine,
-            },
-            dashes: {
-              show: panel.dashes,
-              lineWidth: panel.linewidth,
-              dashLength: [panel.dashLength, panel.spaceLength],
-            },
-            bars: {
-              show: panel.bars,
-              fill: 1,
-              barWidth: 1,
-              zero: false,
-              lineWidth: 0,
-            },
-            points: {
-              show: panel.points,
-              fill: 1,
-              fillColor: false,
-              radius: panel.points ? panel.pointradius : 2,
-            },
-            shadowSize: 0,
-          },
-          yaxes: [],
-          xaxis: {},
-          grid: {
-            minBorderMargin: 0,
-            markings: [],
-            backgroundColor: null,
-            borderWidth: 0,
-            hoverable: true,
-            clickable: true,
-            color: gridColor,
-            margin: { left: 0, right: 0 },
-            labelMarginX: 0,
-          },
-          selection: {
-            mode: 'x',
-            color: '#666',
-          },
-          crosshair: {
-            mode: 'x',
-          },
-        };
-        return options;
-      }
-
-      function sortSeries(series, panel) {
-        var sortBy = panel.legend.sort;
-        var sortOrder = panel.legend.sortDesc;
-        var haveSortBy = sortBy !== null && sortBy !== undefined;
-        var haveSortOrder = sortOrder !== null && sortOrder !== undefined;
-        var shouldSortBy = panel.stack && haveSortBy && haveSortOrder;
-        var sortDesc = panel.legend.sortDesc === true ? -1 : 1;
-
-        if (shouldSortBy) {
-          return _.sortBy(series, s => s.stats[sortBy] * sortDesc);
-        } else {
-          return _.sortBy(series, s => s.zindex);
-        }
-      }
-
-      function translateFillOption(fill) {
-        if (panel.percentage && panel.stack) {
-          return fill === 0 ? 0.001 : fill / 10;
-        } else {
-          return fill / 10;
-        }
-      }
-
-      function addTimeAxis(options) {
-        var ticks = panelWidth / 100;
-        var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
-        var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: 'time',
-          min: min,
-          max: max,
-          label: 'Datetime',
-          ticks: ticks,
-          timeformat: time_format(ticks, min, max),
-        };
-      }
-
-      function addXSeriesAxis(options) {
-        var ticks = _.map(data, function(series, index) {
-          return [index + 1, series.alias];
-        });
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: 0,
-          max: ticks.length + 1,
-          label: 'Datetime',
-          ticks: ticks,
-        };
-      }
-
-      function addXHistogramAxis(options, bucketSize) {
-        let ticks, min, max;
-        let defaultTicks = panelWidth / 50;
-
-        if (data.length && bucketSize) {
-          let tick_values = [];
-          for (let d of data) {
-            for (let point of d.data) {
-              tick_values[point[0]] = true;
-            }
-          }
-          ticks = Object.keys(tick_values).map(v => Number(v));
-          min = _.min(ticks);
-          max = _.max(ticks);
-
-          // Adjust tick step
-          let tickStep = bucketSize;
-          let ticks_num = Math.floor((max - min) / tickStep);
-          while (ticks_num > defaultTicks) {
-            tickStep = tickStep * 2;
-            ticks_num = Math.ceil((max - min) / tickStep);
-          }
-
-          // Expand ticks for pretty view
-          min = Math.floor(min / tickStep) * tickStep;
-          // 1.01 is 101% - ensure we have enough space for last bar
-          max = Math.ceil(max * 1.01 / tickStep) * tickStep;
-
-          ticks = [];
-          for (let i = min; i <= max; i += tickStep) {
-            ticks.push(i);
-          }
-        } else {
-          // Set defaults if no data
-          ticks = defaultTicks / 2;
-          min = 0;
-          max = 1;
-        }
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: min,
-          max: max,
-          label: 'Histogram',
-          ticks: ticks,
-        };
-
-        // Use 'short' format for histogram values
-        configureAxisMode(options.xaxis, 'short');
-      }
-
-      function addXTableAxis(options) {
-        var ticks = _.map(data, function(series, seriesIndex) {
-          return _.map(series.datapoints, function(point, pointIndex) {
-            var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
-            return [tickIndex + 1, point[1]];
-          });
-        });
-        ticks = _.flatten(ticks, true);
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: 0,
-          max: ticks.length + 1,
-          label: 'Datetime',
-          ticks: ticks,
-        };
-      }
-
-      function configureYAxisOptions(data, options) {
-        var defaults = {
-          position: 'left',
-          show: panel.yaxes[0].show,
-          index: 1,
-          logBase: panel.yaxes[0].logBase || 1,
-          min: parseNumber(panel.yaxes[0].min),
-          max: parseNumber(panel.yaxes[0].max),
-          tickDecimals: panel.yaxes[0].decimals,
-        };
-
-        options.yaxes.push(defaults);
-
-        if (_.find(data, { yaxis: 2 })) {
-          var secondY = _.clone(defaults);
-          secondY.index = 2;
-          secondY.show = panel.yaxes[1].show;
-          secondY.logBase = panel.yaxes[1].logBase || 1;
-          secondY.position = 'right';
-          secondY.min = parseNumber(panel.yaxes[1].min);
-          secondY.max = parseNumber(panel.yaxes[1].max);
-          secondY.tickDecimals = panel.yaxes[1].decimals;
-          options.yaxes.push(secondY);
-
-          applyLogScale(options.yaxes[1], data);
-          configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? 'percent' : panel.yaxes[1].format);
-        }
-        applyLogScale(options.yaxes[0], data);
-        configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? 'percent' : panel.yaxes[0].format);
-      }
-
-      function parseNumber(value: any) {
-        if (value === null || typeof value === 'undefined') {
-          return null;
-        }
-
-        return _.toNumber(value);
-      }
-
-      function applyLogScale(axis, data) {
-        if (axis.logBase === 1) {
-          return;
-        }
-
-        const minSetToZero = axis.min === 0;
-
-        if (axis.min < Number.MIN_VALUE) {
-          axis.min = null;
-        }
-        if (axis.max < Number.MIN_VALUE) {
-          axis.max = null;
-        }
-
-        var series, i;
-        var max = axis.max,
-          min = axis.min;
-
-        for (i = 0; i < data.length; i++) {
-          series = data[i];
-          if (series.yaxis === axis.index) {
-            if (!max || max < series.stats.max) {
-              max = series.stats.max;
-            }
-            if (!min || min > series.stats.logmin) {
-              min = series.stats.logmin;
-            }
-          }
-        }
-
-        axis.transform = function(v) {
-          return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase);
-        };
-        axis.inverseTransform = function(v) {
-          return Math.pow(axis.logBase, v);
-        };
-
-        if (!max && !min) {
-          max = axis.inverseTransform(+2);
-          min = axis.inverseTransform(-2);
-        } else if (!max) {
-          max = min * axis.inverseTransform(+4);
-        } else if (!min) {
-          min = max * axis.inverseTransform(-4);
-        }
-
-        if (axis.min) {
-          min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));
-        } else {
-          min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min)));
-        }
-        if (axis.max) {
-          max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));
-        } else {
-          max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max)));
-        }
-
-        if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {
-          return;
-        }
-
-        if (Number.isFinite(min) && Number.isFinite(max)) {
-          if (minSetToZero) {
-            axis.min = 0.1;
-            min = 1;
-          }
-
-          axis.ticks = generateTicksForLogScaleYAxis(min, max, axis.logBase);
-          if (minSetToZero) {
-            axis.ticks.unshift(0.1);
-          }
-          if (axis.ticks[axis.ticks.length - 1] > axis.max) {
-            axis.max = axis.ticks[axis.ticks.length - 1];
-          }
-        } else {
-          axis.ticks = [1, 2];
-          delete axis.min;
-          delete axis.max;
-        }
-      }
-
-      function generateTicksForLogScaleYAxis(min, max, logBase) {
-        let ticks = [];
-
-        var nextTick;
-        for (nextTick = min; nextTick <= max; nextTick *= logBase) {
-          ticks.push(nextTick);
-        }
-
-        const maxNumTicks = Math.ceil(ctrl.height / 25);
-        const numTicks = ticks.length;
-        if (numTicks > maxNumTicks) {
-          const factor = Math.ceil(numTicks / maxNumTicks) * logBase;
-          ticks = [];
-
-          for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {
-            ticks.push(nextTick);
-          }
-        }
-
-        return ticks;
-      }
-
-      function configureAxisMode(axis, format) {
-        axis.tickFormatter = function(val, axis) {
-          if (!kbn.valueFormats[format]) {
-            throw new Error(`Unit '${format}' is not supported`);
-          }
-          return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
-        };
-      }
-
-      function time_format(ticks, min, max) {
-        if (min && max && ticks) {
-          var range = max - min;
-          var secPerTick = range / ticks / 1000;
-          var oneDay = 86400000;
-          var oneYear = 31536000000;
-
-          if (secPerTick <= 45) {
-            return '%H:%M:%S';
-          }
-          if (secPerTick <= 7200 || range <= oneDay) {
-            return '%H:%M';
-          }
-          if (secPerTick <= 80000) {
-            return '%m/%d %H:%M';
-          }
-          if (secPerTick <= 2419200 || range <= oneYear) {
-            return '%m/%d';
-          }
-          return '%Y-%m';
-        }
-
-        return '%H:%M';
-      }
-
-      elem.bind('plotselected', function(event, ranges) {
-        if (panel.xaxis.mode !== 'time') {
-          // Skip if panel in histogram or series mode
-          plot.clearSelection();
-          return;
-        }
-
-        if ((ranges.ctrlKey || ranges.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) {
-          // Add annotation
-          setTimeout(() => {
-            eventManager.updateTime(ranges.xaxis);
-          }, 100);
-        } else {
-          scope.$apply(function() {
-            timeSrv.setTime({
-              from: moment.utc(ranges.xaxis.from),
-              to: moment.utc(ranges.xaxis.to),
-            });
-          });
-        }
-      });
-
-      elem.bind('plotclick', function(event, pos, item) {
-        if (panel.xaxis.mode !== 'time') {
-          // Skip if panel in histogram or series mode
-          return;
-        }
-
-        if ((pos.ctrlKey || pos.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) {
-          // Skip if range selected (added in "plotselected" event handler)
-          let isRangeSelection = pos.x !== pos.x1;
-          if (!isRangeSelection) {
-            setTimeout(() => {
-              eventManager.updateTime({ from: pos.x, to: null });
-            }, 100);
-          }
-        }
-      });
-
-      scope.$on('$destroy', function() {
-        tooltip.destroy();
-        elem.off();
-        elem.remove();
-      });
+    link: (scope, elem) => {
+      return new GraphElement(scope, elem, timeSrv);
     },
   };
 }
 
 coreModule.directive('grafanaGraph', graphDirective);
+export { GraphElement, graphDirective };
diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts
index ef82fb395a5..ba151692147 100644
--- a/public/app/plugins/panel/graph/module.ts
+++ b/public/app/plugins/panel/graph/module.ts
@@ -13,6 +13,7 @@ import { axesEditorComponent } from './axes_editor';
 class GraphCtrl extends MetricsPanelCtrl {
   static template = template;
 
+  renderError: boolean;
   hiddenSeries: any = {};
   seriesList: any = [];
   dataList: any = [];
diff --git a/public/app/plugins/panel/graph/specs/graph.jest.ts b/public/app/plugins/panel/graph/specs/graph.jest.ts
new file mode 100644
index 00000000000..f75f7cd68ea
--- /dev/null
+++ b/public/app/plugins/panel/graph/specs/graph.jest.ts
@@ -0,0 +1,518 @@
+jest.mock('app/features/annotations/all', () => ({
+  EventManager: function() {
+    return {
+      on: () => {},
+      addFlotEvents: () => {},
+    };
+  },
+}));
+
+jest.mock('app/core/core', () => ({
+  coreModule: {
+    directive: () => {},
+  },
+  appEvents: {
+    on: () => {},
+  },
+}));
+
+import '../module';
+import { GraphCtrl } from '../module';
+import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl';
+import { PanelCtrl } from 'app/features/panel/panel_ctrl';
+
+import config from 'app/core/config';
+
+import TimeSeries from 'app/core/time_series2';
+import moment from 'moment';
+import $ from 'jquery';
+import { graphDirective } from '../graph';
+
+let ctx = <any>{};
+let ctrl;
+let scope = {
+  ctrl: {},
+  range: {
+    from: moment([2015, 1, 1]),
+    to: moment([2015, 11, 20]),
+  },
+  $on: () => {},
+};
+let link;
+
+describe('grafanaGraph', function() {
+  const setupCtx = (beforeRender?) => {
+    config.bootData = {
+      user: {
+        lightTheme: false,
+      },
+    };
+    GraphCtrl.prototype = <any>{
+      ...MetricsPanelCtrl.prototype,
+      ...PanelCtrl.prototype,
+      ...GraphCtrl.prototype,
+      height: 200,
+      panel: {
+        events: {
+          on: () => {},
+        },
+        legend: {},
+        grid: {},
+        yaxes: [
+          {
+            min: null,
+            max: null,
+            format: 'short',
+            logBase: 1,
+          },
+          {
+            min: null,
+            max: null,
+            format: 'short',
+            logBase: 1,
+          },
+        ],
+        thresholds: [],
+        xaxis: {},
+        seriesOverrides: [],
+        tooltip: {
+          shared: true,
+        },
+      },
+      renderingCompleted: jest.fn(),
+      hiddenSeries: {},
+      dashboard: {
+        getTimezone: () => 'browser',
+      },
+      range: {
+        from: moment([2015, 1, 1, 10]),
+        to: moment([2015, 1, 1, 22]),
+      },
+    };
+
+    ctx.data = [];
+    ctx.data.push(
+      new TimeSeries({
+        datapoints: [[1, 1], [2, 2]],
+        alias: 'series1',
+      })
+    );
+    ctx.data.push(
+      new TimeSeries({
+        datapoints: [[10, 1], [20, 2]],
+        alias: 'series2',
+      })
+    );
+
+    ctrl = new GraphCtrl(
+      {
+        $on: () => {},
+      },
+      {
+        get: () => {},
+      },
+      {}
+    );
+
+    $.plot = ctrl.plot = jest.fn();
+    scope.ctrl = ctrl;
+
+    link = graphDirective({}, {}, {}).link(scope, { width: () => 500, mouseleave: () => {}, bind: () => {} });
+    if (typeof beforeRender === 'function') {
+      beforeRender();
+    }
+    link.data = ctx.data;
+
+    //Emulate functions called by event listeners
+    link.buildFlotPairs(link.data);
+    link.render_panel();
+    ctx.plotData = ctrl.plot.mock.calls[0][1];
+
+    ctx.plotOptions = ctrl.plot.mock.calls[0][2];
+  };
+
+  describe('simple lines options', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.fill = 5;
+        ctrl.panel.linewidth = 3;
+        ctrl.panel.steppedLine = true;
+      });
+    });
+
+    it('should configure plot with correct options', () => {
+      expect(ctx.plotOptions.series.lines.show).toBe(true);
+      expect(ctx.plotOptions.series.lines.fill).toBe(0.5);
+      expect(ctx.plotOptions.series.lines.lineWidth).toBe(3);
+      expect(ctx.plotOptions.series.lines.steps).toBe(true);
+    });
+  });
+
+  describe('sorting stacked series as legend. disabled', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = undefined;
+        ctrl.panel.stack = false;
+      });
+    });
+
+    it('should not modify order of time series', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. min descending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = true;
+      });
+    });
+    it('highest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('sorting stacked series as legend. min ascending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = false;
+        ctrl.panel.stack = true;
+      });
+    });
+    it('lowest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. stacking disabled', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = false;
+      });
+    });
+
+    it('highest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. current descending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'current';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = true;
+      });
+    });
+
+    it('highest last value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('when logBase is log 10', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
+          alias: 'seriesFixedscale',
+        });
+        ctx.data[1].yaxis = 2;
+        ctrl.panel.yaxes[0].logBase = 10;
+
+        ctrl.panel.yaxes[1].logBase = 10;
+        ctrl.panel.yaxes[1].min = '0.05';
+        ctrl.panel.yaxes[1].max = '1500';
+      });
+    });
+
+    it('should apply axis transform, autoscaling (if necessary) and ticks', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBeCloseTo(0.001);
+      expect(axisAutoscale.max).toBe(10000);
+      expect(axisAutoscale.ticks.length).toBeCloseTo(8);
+      expect(axisAutoscale.ticks[0]).toBeCloseTo(0.001);
+      if (axisAutoscale.ticks.length === 7) {
+        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBeCloseTo(1000);
+      } else {
+        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBe(10000);
+      }
+
+      var axisFixedscale = ctx.plotOptions.yaxes[1];
+      expect(axisFixedscale.min).toBe(0.05);
+      expect(axisFixedscale.max).toBe(1500);
+      expect(axisFixedscale.ticks.length).toBe(5);
+      expect(axisFixedscale.ticks[0]).toBe(0.1);
+      expect(axisFixedscale.ticks[4]).toBe(1000);
+    });
+  });
+
+  describe('when logBase is log 10 and data points contain only zeroes', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.yaxes[0].logBase = 10;
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[0, 1], [0, 2], [0, 3], [0, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+
+    it('should not set min and max and should create some fake ticks', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBe(undefined);
+      expect(axisAutoscale.max).toBe(undefined);
+      expect(axisAutoscale.ticks.length).toBe(2);
+      expect(axisAutoscale.ticks[0]).toBe(1);
+      expect(axisAutoscale.ticks[1]).toBe(2);
+    });
+  });
+
+  // y-min set 0 is a special case for log scale,
+  // this approximates it by setting min to 0.1
+  describe('when logBase is log 10 and y-min is set to 0 and auto min is > 0.1', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.yaxes[0].logBase = 10;
+        ctrl.panel.yaxes[0].min = '0';
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+    it('should set min to 0.1 and add a tick for 0.1', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBe(0.1);
+      expect(axisAutoscale.max).toBe(10000);
+      expect(axisAutoscale.ticks.length).toBe(6);
+      expect(axisAutoscale.ticks[0]).toBe(0.1);
+      expect(axisAutoscale.ticks[5]).toBe(10000);
+    });
+  });
+
+  describe('when logBase is log 2 and y-min is set to 0 and num of ticks exceeds max', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        const heightForApprox5Ticks = 125;
+        ctrl.height = heightForApprox5Ticks;
+        ctrl.panel.yaxes[0].logBase = 2;
+        ctrl.panel.yaxes[0].min = '0';
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4], [10000, 5], [100000, 6]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+
+    it('should regenerate ticks so that if fits on the y-axis', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.min).toBe(0.1);
+      expect(axisAutoscale.ticks.length).toBe(8);
+      expect(axisAutoscale.ticks[0]).toBe(0.1);
+      expect(axisAutoscale.ticks[7]).toBe(262144);
+      expect(axisAutoscale.max).toBe(262144);
+    });
+
+    it('should set axis max to be max tick value', function() {
+      expect(ctx.plotOptions.yaxes[0].max).toBe(262144);
+    });
+  });
+
+  describe('dashed lines options', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.linewidth = 2;
+        ctrl.panel.dashes = true;
+      });
+    });
+
+    it('should configure dashed plot with correct options', function() {
+      expect(ctx.plotOptions.series.lines.show).toBe(true);
+      expect(ctx.plotOptions.series.dashes.lineWidth).toBe(2);
+      expect(ctx.plotOptions.series.dashes.show).toBe(true);
+    });
+  });
+
+  describe('should use timeStep for barWidth', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.bars = true;
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[1, 10], [2, 20]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should set barWidth', function() {
+      expect(ctx.plotOptions.series.bars.barWidth).toBe(1 / 1.5);
+    });
+  });
+
+  describe('series option overrides, fill & points', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.fill = 5;
+        ctx.data[0].zindex = 10;
+        ctx.data[1].alias = 'test';
+        ctx.data[1].lines = { fill: 0.001 };
+        ctx.data[1].points = { show: true };
+      });
+    });
+
+    it('should match second series and fill zero, and enable points', function() {
+      expect(ctx.plotOptions.series.lines.fill).toBe(0.5);
+      expect(ctx.plotData[1].lines.fill).toBe(0.001);
+      expect(ctx.plotData[1].points.show).toBe(true);
+    });
+  });
+
+  describe('should order series order according to zindex', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctx.data[1].zindex = 1;
+        ctx.data[0].zindex = 10;
+      });
+    });
+
+    it('should move zindex 2 last', function() {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('when series is hidden', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.hiddenSeries = { series2: true };
+      });
+    });
+
+    it('should remove datapoints and disable stack', function() {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].data.length).toBe(0);
+      expect(ctx.plotData[1].stack).toBe(false);
+    });
+  });
+
+  describe('when stack and percent', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.percentage = true;
+        ctrl.panel.stack = true;
+      });
+    });
+
+    it('should show percentage', function() {
+      var axis = ctx.plotOptions.yaxes[0];
+      expect(axis.tickFormatter(100, axis)).toBe('100%');
+    });
+  });
+
+  describe('when panel too narrow to show x-axis dates in same granularity as wide panels', () => {
+    //Set width to 10px
+    describe('and the range is less than 24 hours', function() {
+      beforeEach(() => {
+        setupCtx(() => {
+          ctrl.range.from = moment([2015, 1, 1, 10]);
+          ctrl.range.to = moment([2015, 1, 1, 22]);
+        });
+      });
+
+      it('should format dates as hours minutes', function() {
+        var axis = ctx.plotOptions.xaxis;
+        expect(axis.timeformat).toBe('%H:%M');
+      });
+    });
+
+    describe('and the range is less than one year', function() {
+      beforeEach(() => {
+        setupCtx(() => {
+          ctrl.range.from = moment([2015, 1, 1]);
+          ctrl.range.to = moment([2015, 11, 20]);
+        });
+      });
+
+      it('should format dates as month days', function() {
+        var axis = ctx.plotOptions.xaxis;
+        expect(axis.timeformat).toBe('%m/%d');
+      });
+    });
+  });
+
+  describe('when graph is histogram, and enable stack', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.stack = true;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series2',
+        });
+      });
+    });
+
+    it('should calculate correct histogram', function() {
+      expect(ctx.plotData[0].data[0][0]).toBe(100);
+      expect(ctx.plotData[0].data[0][1]).toBe(2);
+      expect(ctx.plotData[1].data[0][0]).toBe(100);
+      expect(ctx.plotData[1].data[0][1]).toBe(2);
+    });
+  });
+
+  describe('when graph is histogram, and some series are hidden', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = { series2: true };
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series2',
+        });
+      });
+    });
+
+    it('should calculate correct histogram', function() {
+      expect(ctx.plotData[0].data[0][0]).toBe(100);
+      expect(ctx.plotData[0].data[0][1]).toBe(2);
+    });
+  });
+});
diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts
deleted file mode 100644
index d29320a9d72..00000000000
--- a/public/app/plugins/panel/graph/specs/graph_specs.ts
+++ /dev/null
@@ -1,454 +0,0 @@
-import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
-
-import '../module';
-import angular from 'angular';
-import $ from 'jquery';
-import helpers from 'test/specs/helpers';
-import TimeSeries from 'app/core/time_series2';
-import moment from 'moment';
-import { Emitter } from 'app/core/core';
-
-describe('grafanaGraph', function() {
-  beforeEach(angularMocks.module('grafana.core'));
-
-  function graphScenario(desc, func, elementWidth = 500) {
-    describe(desc, () => {
-      var ctx: any = {};
-
-      ctx.setup = setupFunc => {
-        beforeEach(
-          angularMocks.module($provide => {
-            $provide.value('timeSrv', new helpers.TimeSrvStub());
-          })
-        );
-
-        beforeEach(
-          angularMocks.inject(($rootScope, $compile) => {
-            var ctrl: any = {
-              height: 200,
-              panel: {
-                events: new Emitter(),
-                legend: {},
-                grid: {},
-                yaxes: [
-                  {
-                    min: null,
-                    max: null,
-                    format: 'short',
-                    logBase: 1,
-                  },
-                  {
-                    min: null,
-                    max: null,
-                    format: 'short',
-                    logBase: 1,
-                  },
-                ],
-                thresholds: [],
-                xaxis: {},
-                seriesOverrides: [],
-                tooltip: {
-                  shared: true,
-                },
-              },
-              renderingCompleted: sinon.spy(),
-              hiddenSeries: {},
-              dashboard: {
-                getTimezone: sinon.stub().returns('browser'),
-              },
-              range: {
-                from: moment([2015, 1, 1, 10]),
-                to: moment([2015, 1, 1, 22]),
-              },
-            };
-
-            var scope = $rootScope.$new();
-            scope.ctrl = ctrl;
-            scope.ctrl.events = ctrl.panel.events;
-
-            $rootScope.onAppEvent = sinon.spy();
-
-            ctx.data = [];
-            ctx.data.push(
-              new TimeSeries({
-                datapoints: [[1, 1], [2, 2]],
-                alias: 'series1',
-              })
-            );
-            ctx.data.push(
-              new TimeSeries({
-                datapoints: [[10, 1], [20, 2]],
-                alias: 'series2',
-              })
-            );
-
-            setupFunc(ctrl, ctx.data);
-
-            var element = angular.element("<div style='width:" + elementWidth + "px' grafana-graph><div>");
-            $compile(element)(scope);
-            scope.$digest();
-
-            $.plot = ctx.plotSpy = sinon.spy();
-            ctrl.events.emit('render', ctx.data);
-            ctrl.events.emit('render-legend');
-            ctrl.events.emit('legend-rendering-complete');
-            ctx.plotData = ctx.plotSpy.getCall(0).args[1];
-            ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
-          })
-        );
-      };
-
-      func(ctx);
-    });
-  }
-
-  graphScenario('simple lines options', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.lines = true;
-      ctrl.panel.fill = 5;
-      ctrl.panel.linewidth = 3;
-      ctrl.panel.steppedLine = true;
-    });
-
-    it('should configure plot with correct options', () => {
-      expect(ctx.plotOptions.series.lines.show).to.be(true);
-      expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
-      expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
-      expect(ctx.plotOptions.series.lines.steps).to.be(true);
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. disabled', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = undefined;
-      ctrl.panel.stack = false;
-    });
-
-    it('should not modify order of time series', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. min descending order', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('highest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. min ascending order', ctx => {
-    ctx.setup((ctrl, data) => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = false;
-      ctrl.panel.stack = true;
-    });
-
-    it('lowest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. stacking disabled', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = false;
-    });
-
-    it('highest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. current descending order', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'current';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('highest last value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('when logBase is log 10', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-      ctrl.panel.yaxes[1].logBase = 10;
-      ctrl.panel.yaxes[1].min = '0.05';
-      ctrl.panel.yaxes[1].max = '1500';
-      data[1] = new TimeSeries({
-        datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
-        alias: 'seriesFixedscale',
-      });
-      data[1].yaxis = 2;
-    });
-
-    it('should apply axis transform, autoscaling (if necessary) and ticks', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.max).to.be(10000);
-      expect(axisAutoscale.ticks.length).to.within(7, 8);
-      expect(axisAutoscale.ticks[0]).to.within(0.00099999999, 0.00100000001);
-      if (axisAutoscale.ticks.length === 7) {
-        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).to.within(999.9999, 1000.0001);
-      } else {
-        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).to.be(10000);
-      }
-
-      var axisFixedscale = ctx.plotOptions.yaxes[1];
-      expect(axisFixedscale.min).to.be(0.05);
-      expect(axisFixedscale.max).to.be(1500);
-      expect(axisFixedscale.ticks.length).to.be(5);
-      expect(axisFixedscale.ticks[0]).to.be(0.1);
-      expect(axisFixedscale.ticks[4]).to.be(1000);
-    });
-  });
-
-  graphScenario('when logBase is log 10 and data points contain only zeroes', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      data[0] = new TimeSeries({
-        datapoints: [[0, 1], [0, 2], [0, 3], [0, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should not set min and max and should create some fake ticks', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.be(undefined);
-      expect(axisAutoscale.max).to.be(undefined);
-      expect(axisAutoscale.ticks.length).to.be(2);
-      expect(axisAutoscale.ticks[0]).to.be(1);
-      expect(axisAutoscale.ticks[1]).to.be(2);
-    });
-  });
-
-  // y-min set 0 is a special case for log scale,
-  // this approximates it by setting min to 0.1
-  graphScenario('when logBase is log 10 and y-min is set to 0 and auto min is > 0.1', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      ctrl.panel.yaxes[0].min = '0';
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should set min to 0.1 and add a tick for 0.1', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.be(0.1);
-      expect(axisAutoscale.max).to.be(10000);
-      expect(axisAutoscale.ticks.length).to.be(6);
-      expect(axisAutoscale.ticks[0]).to.be(0.1);
-      expect(axisAutoscale.ticks[5]).to.be(10000);
-    });
-  });
-
-  graphScenario('when logBase is log 2 and y-min is set to 0 and num of ticks exceeds max', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      const heightForApprox5Ticks = 125;
-      ctrl.height = heightForApprox5Ticks;
-      ctrl.panel.yaxes[0].logBase = 2;
-      ctrl.panel.yaxes[0].min = '0';
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4], [10000, 5], [100000, 6]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should regenerate ticks so that if fits on the y-axis', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.min).to.be(0.1);
-      expect(axisAutoscale.ticks.length).to.be(8);
-      expect(axisAutoscale.ticks[0]).to.be(0.1);
-      expect(axisAutoscale.ticks[7]).to.be(262144);
-      expect(axisAutoscale.max).to.be(262144);
-    });
-
-    it('should set axis max to be max tick value', function() {
-      expect(ctx.plotOptions.yaxes[0].max).to.be(262144);
-    });
-  });
-
-  graphScenario('dashed lines options', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.lines = true;
-      ctrl.panel.linewidth = 2;
-      ctrl.panel.dashes = true;
-    });
-
-    it('should configure dashed plot with correct options', function() {
-      expect(ctx.plotOptions.series.lines.show).to.be(true);
-      expect(ctx.plotOptions.series.dashes.lineWidth).to.be(2);
-      expect(ctx.plotOptions.series.dashes.show).to.be(true);
-    });
-  });
-
-  graphScenario('should use timeStep for barWidth', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.bars = true;
-      data[0] = new TimeSeries({
-        datapoints: [[1, 10], [2, 20]],
-        alias: 'series1',
-      });
-    });
-
-    it('should set barWidth', function() {
-      expect(ctx.plotOptions.series.bars.barWidth).to.be(1 / 1.5);
-    });
-  });
-
-  graphScenario('series option overrides, fill & points', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.lines = true;
-      ctrl.panel.fill = 5;
-      data[0].zindex = 10;
-      data[1].alias = 'test';
-      data[1].lines = { fill: 0.001 };
-      data[1].points = { show: true };
-    });
-
-    it('should match second series and fill zero, and enable points', function() {
-      expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
-      expect(ctx.plotData[1].lines.fill).to.be(0.001);
-      expect(ctx.plotData[1].points.show).to.be(true);
-    });
-  });
-
-  graphScenario('should order series order according to zindex', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      data[1].zindex = 1;
-      data[0].zindex = 10;
-    });
-
-    it('should move zindex 2 last', function() {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('when series is hidden', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.hiddenSeries = { series2: true };
-    });
-
-    it('should remove datapoints and disable stack', function() {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].data.length).to.be(0);
-      expect(ctx.plotData[1].stack).to.be(false);
-    });
-  });
-
-  graphScenario('when stack and percent', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.percentage = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('should show percentage', function() {
-      var axis = ctx.plotOptions.yaxes[0];
-      expect(axis.tickFormatter(100, axis)).to.be('100%');
-    });
-  });
-
-  graphScenario(
-    'when panel too narrow to show x-axis dates in same granularity as wide panels',
-    function(ctx) {
-      describe('and the range is less than 24 hours', function() {
-        ctx.setup(function(ctrl) {
-          ctrl.range.from = moment([2015, 1, 1, 10]);
-          ctrl.range.to = moment([2015, 1, 1, 22]);
-        });
-
-        it('should format dates as hours minutes', function() {
-          var axis = ctx.plotOptions.xaxis;
-          expect(axis.timeformat).to.be('%H:%M');
-        });
-      });
-
-      describe('and the range is less than one year', function() {
-        ctx.setup(function(scope) {
-          scope.range.from = moment([2015, 1, 1]);
-          scope.range.to = moment([2015, 11, 20]);
-        });
-
-        it('should format dates as month days', function() {
-          var axis = ctx.plotOptions.xaxis;
-          expect(axis.timeformat).to.be('%m/%d');
-        });
-      });
-    },
-    10
-  );
-
-  graphScenario('when graph is histogram, and enable stack', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.xaxis.mode = 'histogram';
-      ctrl.panel.stack = true;
-      ctrl.hiddenSeries = {};
-      data[0] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series1',
-      });
-      data[1] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series2',
-      });
-    });
-
-    it('should calculate correct histogram', function() {
-      expect(ctx.plotData[0].data[0][0]).to.be(100);
-      expect(ctx.plotData[0].data[0][1]).to.be(2);
-      expect(ctx.plotData[1].data[0][0]).to.be(100);
-      expect(ctx.plotData[1].data[0][1]).to.be(2);
-    });
-  });
-
-  graphScenario('when graph is histogram, and some series are hidden', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.xaxis.mode = 'histogram';
-      ctrl.panel.stack = false;
-      ctrl.hiddenSeries = { series2: true };
-      data[0] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series1',
-      });
-      data[1] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series2',
-      });
-    });
-
-    it('should calculate correct histogram', function() {
-      expect(ctx.plotData[0].data[0][0]).to.be(100);
-      expect(ctx.plotData[0].data[0][1]).to.be(2);
-    });
-  });
-});

From 35694a76efbff0ebff57c1af7c6ecbc0a8365fc2 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Wed, 1 Aug 2018 17:11:29 +0200
Subject: [PATCH 310/380] Class to function. Half tests passing

---
 .../app/features/dashboard/shareModalCtrl.ts  | 180 +++++++++---------
 .../dashboard/specs/share_modal_ctrl.jest.ts  | 154 +++++++++++++++
 2 files changed, 243 insertions(+), 91 deletions(-)
 create mode 100644 public/app/features/dashboard/specs/share_modal_ctrl.jest.ts

diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts
index 985c20f03b2..c32c2a79190 100644
--- a/public/app/features/dashboard/shareModalCtrl.ts
+++ b/public/app/features/dashboard/shareModalCtrl.ts
@@ -2,120 +2,118 @@ import angular from 'angular';
 import config from 'app/core/config';
 import moment from 'moment';
 
-export class ShareModalCtrl {
-  /** @ngInject */
-  constructor($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
-    $scope.options = {
-      forCurrent: true,
-      includeTemplateVars: true,
-      theme: 'current',
-    };
-    $scope.editor = { index: $scope.tabIndex || 0 };
+/** @ngInject */
+export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
+  $scope.options = {
+    forCurrent: true,
+    includeTemplateVars: true,
+    theme: 'current',
+  };
+  $scope.editor = { index: $scope.tabIndex || 0 };
 
-    $scope.init = function() {
-      $scope.modeSharePanel = $scope.panel ? true : false;
+  $scope.init = function() {
+    $scope.modeSharePanel = $scope.panel ? true : false;
 
-      $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }];
+    $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }];
 
-      if ($scope.modeSharePanel) {
-        $scope.modalTitle = 'Share Panel';
-        $scope.tabs.push({ title: 'Embed', src: 'shareEmbed.html' });
-      } else {
-        $scope.modalTitle = 'Share';
-      }
+    if ($scope.modeSharePanel) {
+      $scope.modalTitle = 'Share Panel';
+      $scope.tabs.push({ title: 'Embed', src: 'shareEmbed.html' });
+    } else {
+      $scope.modalTitle = 'Share';
+    }
 
-      if (!$scope.dashboard.meta.isSnapshot) {
-        $scope.tabs.push({ title: 'Snapshot', src: 'shareSnapshot.html' });
-      }
+    if (!$scope.dashboard.meta.isSnapshot) {
+      $scope.tabs.push({ title: 'Snapshot', src: 'shareSnapshot.html' });
+    }
 
-      if (!$scope.dashboard.meta.isSnapshot && !$scope.modeSharePanel) {
-        $scope.tabs.push({ title: 'Export', src: 'shareExport.html' });
-      }
+    if (!$scope.dashboard.meta.isSnapshot && !$scope.modeSharePanel) {
+      $scope.tabs.push({ title: 'Export', src: 'shareExport.html' });
+    }
 
-      $scope.buildUrl();
-    };
+    $scope.buildUrl();
+  };
 
-    $scope.buildUrl = function() {
-      var baseUrl = $location.absUrl();
-      var queryStart = baseUrl.indexOf('?');
+  $scope.buildUrl = function() {
+    var baseUrl = $location.absUrl();
+    var queryStart = baseUrl.indexOf('?');
 
-      if (queryStart !== -1) {
-        baseUrl = baseUrl.substring(0, queryStart);
-      }
+    if (queryStart !== -1) {
+      baseUrl = baseUrl.substring(0, queryStart);
+    }
 
-      var params = angular.copy($location.search());
+    var params = angular.copy($location.search());
 
-      var range = timeSrv.timeRange();
-      params.from = range.from.valueOf();
-      params.to = range.to.valueOf();
-      params.orgId = config.bootData.user.orgId;
+    var range = timeSrv.timeRange();
+    params.from = range.from.valueOf();
+    params.to = range.to.valueOf();
+    params.orgId = config.bootData.user.orgId;
 
-      if ($scope.options.includeTemplateVars) {
-        templateSrv.fillVariableValuesForUrl(params);
-      }
+    if ($scope.options.includeTemplateVars) {
+      templateSrv.fillVariableValuesForUrl(params);
+    }
 
-      if (!$scope.options.forCurrent) {
-        delete params.from;
-        delete params.to;
-      }
+    if (!$scope.options.forCurrent) {
+      delete params.from;
+      delete params.to;
+    }
 
-      if ($scope.options.theme !== 'current') {
-        params.theme = $scope.options.theme;
-      }
+    if ($scope.options.theme !== 'current') {
+      params.theme = $scope.options.theme;
+    }
 
-      if ($scope.modeSharePanel) {
-        params.panelId = $scope.panel.id;
-        params.fullscreen = true;
-      } else {
-        delete params.panelId;
-        delete params.fullscreen;
-      }
-
-      $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
-
-      var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
-      soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
+    if ($scope.modeSharePanel) {
+      params.panelId = $scope.panel.id;
+      params.fullscreen = true;
+    } else {
+      delete params.panelId;
       delete params.fullscreen;
-      delete params.edit;
-      soloUrl = linkSrv.addParamsToUrl(soloUrl, params);
+    }
 
-      $scope.iframeHtml = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
+    $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
 
-      $scope.imageUrl = soloUrl.replace(
-        config.appSubUrl + '/dashboard-solo/',
-        config.appSubUrl + '/render/dashboard-solo/'
-      );
-      $scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/');
-      $scope.imageUrl += '&width=1000&height=500' + $scope.getLocalTimeZone();
-    };
+    var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
+    soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
+    delete params.fullscreen;
+    delete params.edit;
+    soloUrl = linkSrv.addParamsToUrl(soloUrl, params);
 
-    // This function will try to return the proper full name of the local timezone
-    // Chrome does not handle the timezone offset (but phantomjs does)
-    $scope.getLocalTimeZone = function() {
-      let utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z'));
+    $scope.iframeHtml = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
 
-      // Older browser does not the internationalization API
-      if (!(<any>window).Intl) {
-        return utcOffset;
-      }
+    $scope.imageUrl = soloUrl.replace(
+      config.appSubUrl + '/dashboard-solo/',
+      config.appSubUrl + '/render/dashboard-solo/'
+    );
+    $scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/');
+    $scope.imageUrl += '&width=1000&height=500' + $scope.getLocalTimeZone();
+  };
 
-      const dateFormat = (<any>window).Intl.DateTimeFormat();
-      if (!dateFormat.resolvedOptions) {
-        return utcOffset;
-      }
+  // This function will try to return the proper full name of the local timezone
+  // Chrome does not handle the timezone offset (but phantomjs does)
+  $scope.getLocalTimeZone = function() {
+    let utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z'));
 
-      const options = dateFormat.resolvedOptions();
-      if (!options.timeZone) {
-        return utcOffset;
-      }
+    // Older browser does not the internationalization API
+    if (!(<any>window).Intl) {
+      return utcOffset;
+    }
 
-      return '&tz=' + encodeURIComponent(options.timeZone);
-    };
+    const dateFormat = (<any>window).Intl.DateTimeFormat();
+    if (!dateFormat.resolvedOptions) {
+      return utcOffset;
+    }
 
-    $scope.getShareUrl = function() {
-      return $scope.shareUrl;
-    };
-  }
+    const options = dateFormat.resolvedOptions();
+    if (!options.timeZone) {
+      return utcOffset;
+    }
+
+    return '&tz=' + encodeURIComponent(options.timeZone);
+  };
+
+  $scope.getShareUrl = function() {
+    return $scope.shareUrl;
+  };
 }
 
 angular.module('grafana.controllers').controller('ShareModalCtrl', ShareModalCtrl);
diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
new file mode 100644
index 00000000000..47b2a2189cd
--- /dev/null
+++ b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
@@ -0,0 +1,154 @@
+import '../shareModalCtrl';
+import { ShareModalCtrl } from '../shareModalCtrl';
+import config from 'app/core/config';
+import { LinkSrv } from 'app/features/panellinks/link_srv';
+
+describe('ShareModalCtrl', () => {
+  var ctx = <any>{
+    timeSrv: {
+      timeRange: () => {
+        return { from: new Date(1000), to: new Date(2000) };
+      },
+    },
+    $location: {
+      absUrl: () => 'http://server/#!/test',
+      search: () => {
+        return { from: '', to: '' };
+      },
+    },
+    scope: {
+      dashboard: {
+        meta: {
+          isSnapshot: true,
+        },
+      },
+    },
+    templateSrv: {
+      fillVariableValuesForUrl: () => {},
+    },
+  };
+  //   function setTime(range) {
+  //     ctx.timeSrv.timeRange = () => range;
+  //   }
+
+  beforeEach(() => {
+    config.bootData = {
+      user: {
+        orgId: 1,
+      },
+    };
+  });
+
+  //   setTime({ from: new Date(1000), to: new Date(2000) });
+
+  //   beforeEach(angularMocks.module('grafana.controllers'));
+  //   beforeEach(angularMocks.module('grafana.services'));
+  //   beforeEach(
+  //     angularMocks.module(function($compileProvider) {
+  //       $compileProvider.preAssignBindingsEnabled(true);
+  //     })
+  //   );
+
+  //   beforeEach(ctx.providePhase());
+
+  //   beforeEach(ctx.createControllerPhase('ShareModalCtrl'));
+  beforeEach(() => {
+    ctx.ctrl = new ShareModalCtrl(
+      ctx.scope,
+      {},
+      ctx.$location,
+      {},
+      ctx.timeSrv,
+      ctx.templateSrv,
+      new LinkSrv({}, ctx.stimeSrv)
+    );
+  });
+
+  describe('shareUrl with current time range and panel', () => {
+    it('should generate share url absolute time', () => {
+      //   ctx.$location.path('/test');
+      ctx.scope.panel = { id: 22 };
+
+      ctx.scope.init();
+      expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&panelId=22&fullscreen');
+    });
+
+    it('should generate render url', () => {
+      ctx.$location.absUrl = () => 'http://dashboards.grafana.com/d/abcdefghi/my-dash';
+
+      ctx.scope.panel = { id: 22 };
+
+      ctx.scope.init();
+      var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
+      var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
+      expect(ctx.scope.imageUrl).toContain(base + params);
+    });
+
+    it('should generate render url for scripted dashboard', () => {
+      ctx.$location.absUrl = () => 'http://dashboards.grafana.com/dashboard/script/my-dash.js';
+
+      ctx.scope.panel = { id: 22 };
+
+      ctx.scope.init();
+      var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js';
+      var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
+      expect(ctx.scope.imageUrl).toContain(base + params);
+    });
+
+    it('should remove panel id when no panel in scope', () => {
+      //   ctx.$location.path('/test');
+      ctx.$location.absUrl = () => 'http://server/#!/test';
+      ctx.scope.options.forCurrent = true;
+      ctx.scope.panel = null;
+
+      ctx.scope.init();
+      expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1');
+    });
+
+    it('should add theme when specified', () => {
+      //   ctx.$location.path('/test');
+      ctx.scope.options.theme = 'light';
+      ctx.scope.panel = null;
+
+      ctx.scope.init();
+      expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light');
+    });
+
+    it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', () => {
+      ctx.$location.absUrl = () => 'http://server/#!/test?fullscreen&edit';
+      ctx.scope.modeSharePanel = true;
+      ctx.scope.panel = { id: 1 };
+
+      ctx.scope.buildUrl();
+
+      expect(ctx.scope.shareUrl).toContain('?fullscreen&edit&from=1000&to=2000&orgId=1&panelId=1');
+      expect(ctx.scope.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
+    });
+
+    it('should remove edit from image url when is first param in querystring and modeSharePanel is true', () => {
+      ctx.$location.absUrl = () => 'http://server/#!/test?edit&fullscreen';
+      ctx.scope.modeSharePanel = true;
+      ctx.scope.panel = { id: 1 };
+
+      ctx.scope.buildUrl();
+
+      expect(ctx.scope.shareUrl).toContain('?edit&fullscreen&from=1000&to=2000&orgId=1&panelId=1');
+      expect(ctx.scope.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
+    });
+
+    it('should include template variables in url', () => {
+      ctx.$location.absUrl = () => 'http://server/#!/test';
+      ctx.scope.options.includeTemplateVars = true;
+
+      ctx.templateSrv.fillVariableValuesForUrl = function(params) {
+        params['var-app'] = 'mupp';
+        params['var-server'] = 'srv-01';
+      };
+
+      ctx.scope.buildUrl();
+      expect(ctx.scope.shareUrl).toContain(
+        'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01'
+      );
+    });
+  });
+});

From 38422ce8a4128da4c4ff7370d5a3e7becaf0e588 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 14:37:31 +0200
Subject: [PATCH 311/380] All tests passing

---
 .../dashboard/specs/share_modal_ctrl.jest.ts  | 21 +++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
index 47b2a2189cd..31f09a6c08a 100644
--- a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
+++ b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
@@ -27,6 +27,14 @@ describe('ShareModalCtrl', () => {
       fillVariableValuesForUrl: () => {},
     },
   };
+
+  (<any>window).Intl.DateTimeFormat = () => {
+    return {
+      resolvedOptions: () => {
+        return { timeZone: 'UTC' };
+      },
+    };
+  };
   //   function setTime(range) {
   //     ctx.timeSrv.timeRange = () => range;
   //   }
@@ -48,10 +56,6 @@ describe('ShareModalCtrl', () => {
   //       $compileProvider.preAssignBindingsEnabled(true);
   //     })
   //   );
-
-  //   beforeEach(ctx.providePhase());
-
-  //   beforeEach(ctx.createControllerPhase('ShareModalCtrl'));
   beforeEach(() => {
     ctx.ctrl = new ShareModalCtrl(
       ctx.scope,
@@ -115,6 +119,9 @@ describe('ShareModalCtrl', () => {
     });
 
     it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', () => {
+      ctx.$location.search = () => {
+        return { fullscreen: true, edit: true };
+      };
       ctx.$location.absUrl = () => 'http://server/#!/test?fullscreen&edit';
       ctx.scope.modeSharePanel = true;
       ctx.scope.panel = { id: 1 };
@@ -126,6 +133,9 @@ describe('ShareModalCtrl', () => {
     });
 
     it('should remove edit from image url when is first param in querystring and modeSharePanel is true', () => {
+      ctx.$location.search = () => {
+        return { edit: true, fullscreen: true };
+      };
       ctx.$location.absUrl = () => 'http://server/#!/test?edit&fullscreen';
       ctx.scope.modeSharePanel = true;
       ctx.scope.panel = { id: 1 };
@@ -137,6 +147,9 @@ describe('ShareModalCtrl', () => {
     });
 
     it('should include template variables in url', () => {
+      ctx.$location.search = () => {
+        return {};
+      };
       ctx.$location.absUrl = () => 'http://server/#!/test';
       ctx.scope.options.includeTemplateVars = true;
 

From be7b663369386689a62801b06bfaaafabaff8e52 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 14:40:44 +0200
Subject: [PATCH 312/380] Cleanup

---
 .../dashboard/specs/share_modal_ctrl.jest.ts  |  16 ---
 .../dashboard/specs/share_modal_ctrl_specs.ts | 122 ------------------
 2 files changed, 138 deletions(-)
 delete mode 100644 public/app/features/dashboard/specs/share_modal_ctrl_specs.ts

diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
index 31f09a6c08a..e5b5340aca5 100644
--- a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
+++ b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
@@ -35,9 +35,6 @@ describe('ShareModalCtrl', () => {
       },
     };
   };
-  //   function setTime(range) {
-  //     ctx.timeSrv.timeRange = () => range;
-  //   }
 
   beforeEach(() => {
     config.bootData = {
@@ -45,18 +42,7 @@ describe('ShareModalCtrl', () => {
         orgId: 1,
       },
     };
-  });
 
-  //   setTime({ from: new Date(1000), to: new Date(2000) });
-
-  //   beforeEach(angularMocks.module('grafana.controllers'));
-  //   beforeEach(angularMocks.module('grafana.services'));
-  //   beforeEach(
-  //     angularMocks.module(function($compileProvider) {
-  //       $compileProvider.preAssignBindingsEnabled(true);
-  //     })
-  //   );
-  beforeEach(() => {
     ctx.ctrl = new ShareModalCtrl(
       ctx.scope,
       {},
@@ -100,7 +86,6 @@ describe('ShareModalCtrl', () => {
     });
 
     it('should remove panel id when no panel in scope', () => {
-      //   ctx.$location.path('/test');
       ctx.$location.absUrl = () => 'http://server/#!/test';
       ctx.scope.options.forCurrent = true;
       ctx.scope.panel = null;
@@ -110,7 +95,6 @@ describe('ShareModalCtrl', () => {
     });
 
     it('should add theme when specified', () => {
-      //   ctx.$location.path('/test');
       ctx.scope.options.theme = 'light';
       ctx.scope.panel = null;
 
diff --git a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts
deleted file mode 100644
index fc70a54a41c..00000000000
--- a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common';
-import helpers from 'test/specs/helpers';
-import '../shareModalCtrl';
-import config from 'app/core/config';
-import 'app/features/panellinks/link_srv';
-
-describe('ShareModalCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
-
-  function setTime(range) {
-    ctx.timeSrv.timeRange = sinon.stub().returns(range);
-  }
-
-  beforeEach(function() {
-    config.bootData = {
-      user: {
-        orgId: 1,
-      },
-    };
-  });
-
-  setTime({ from: new Date(1000), to: new Date(2000) });
-
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
-
-  beforeEach(ctx.providePhase());
-
-  beforeEach(ctx.createControllerPhase('ShareModalCtrl'));
-
-  describe('shareUrl with current time range and panel', function() {
-    it('should generate share url absolute time', function() {
-      ctx.$location.path('/test');
-      ctx.scope.panel = { id: 22 };
-
-      ctx.scope.init();
-      expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&panelId=22&fullscreen');
-    });
-
-    it('should generate render url', function() {
-      ctx.$location.$$absUrl = 'http://dashboards.grafana.com/d/abcdefghi/my-dash';
-
-      ctx.scope.panel = { id: 22 };
-
-      ctx.scope.init();
-      var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
-      var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
-      expect(ctx.scope.imageUrl).to.contain(base + params);
-    });
-
-    it('should generate render url for scripted dashboard', function() {
-      ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/script/my-dash.js';
-
-      ctx.scope.panel = { id: 22 };
-
-      ctx.scope.init();
-      var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js';
-      var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
-      expect(ctx.scope.imageUrl).to.contain(base + params);
-    });
-
-    it('should remove panel id when no panel in scope', function() {
-      ctx.$location.path('/test');
-      ctx.scope.options.forCurrent = true;
-      ctx.scope.panel = null;
-
-      ctx.scope.init();
-      expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1');
-    });
-
-    it('should add theme when specified', function() {
-      ctx.$location.path('/test');
-      ctx.scope.options.theme = 'light';
-      ctx.scope.panel = null;
-
-      ctx.scope.init();
-      expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light');
-    });
-
-    it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', function() {
-      ctx.$location.url('/test?fullscreen&edit');
-      ctx.scope.modeSharePanel = true;
-      ctx.scope.panel = { id: 1 };
-
-      ctx.scope.buildUrl();
-
-      expect(ctx.scope.shareUrl).to.contain('?fullscreen&edit&from=1000&to=2000&orgId=1&panelId=1');
-      expect(ctx.scope.imageUrl).to.contain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
-    });
-
-    it('should remove edit from image url when is first param in querystring and modeSharePanel is true', function() {
-      ctx.$location.url('/test?edit&fullscreen');
-      ctx.scope.modeSharePanel = true;
-      ctx.scope.panel = { id: 1 };
-
-      ctx.scope.buildUrl();
-
-      expect(ctx.scope.shareUrl).to.contain('?edit&fullscreen&from=1000&to=2000&orgId=1&panelId=1');
-      expect(ctx.scope.imageUrl).to.contain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
-    });
-
-    it('should include template variables in url', function() {
-      ctx.$location.path('/test');
-      ctx.scope.options.includeTemplateVars = true;
-
-      ctx.templateSrv.fillVariableValuesForUrl = function(params) {
-        params['var-app'] = 'mupp';
-        params['var-server'] = 'srv-01';
-      };
-
-      ctx.scope.buildUrl();
-      expect(ctx.scope.shareUrl).to.be(
-        'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01'
-      );
-    });
-  });
-});

From fa6d25af72f8191dc67f2948c6748def69b1a8c1 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Fri, 3 Aug 2018 14:44:40 +0200
Subject: [PATCH 313/380] Remove comment

---
 public/app/features/dashboard/specs/share_modal_ctrl.jest.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
index e5b5340aca5..35261256566 100644
--- a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
+++ b/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
@@ -56,7 +56,6 @@ describe('ShareModalCtrl', () => {
 
   describe('shareUrl with current time range and panel', () => {
     it('should generate share url absolute time', () => {
-      //   ctx.$location.path('/test');
       ctx.scope.panel = { id: 22 };
 
       ctx.scope.init();

From 2459b177f914a12438424cf068638b9ce107d115 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Mon, 13 Aug 2018 18:09:01 +0200
Subject: [PATCH 314/380] change: Set User-Agent to Grafana/%Version%
 Proxied-DS-Request %DS-Type% in all proxied ds requests

---
 pkg/api/pluginproxy/ds_proxy.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go
index b420398f9a9..74ad4e226fd 100644
--- a/pkg/api/pluginproxy/ds_proxy.go
+++ b/pkg/api/pluginproxy/ds_proxy.go
@@ -203,6 +203,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
 		req.Header.Del("X-Forwarded-Host")
 		req.Header.Del("X-Forwarded-Port")
 		req.Header.Del("X-Forwarded-Proto")
+		req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s Proxied-DS-Request %s", setting.BuildVersion, proxy.ds.Type))
 
 		// set X-Forwarded-For header
 		if req.RemoteAddr != "" {

From 3552a4cb86151c91ecbf0b2d3265761b276dbaa6 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 14 Aug 2018 08:34:20 +0200
Subject: [PATCH 315/380] refactor timescaledb handling in MacroEngine

---
 pkg/tsdb/postgres/macros.go        | 15 +++++++++------
 pkg/tsdb/postgres/macros_test.go   | 14 ++++++++------
 pkg/tsdb/postgres/postgres.go      |  2 +-
 pkg/tsdb/postgres/postgres_test.go | 22 ----------------------
 4 files changed, 18 insertions(+), 35 deletions(-)

diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index d9f97e9262c..81b0da9fbce 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
@@ -15,12 +16,15 @@ const rsIdentifier = `([_a-zA-Z0-9]+)`
 const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)`
 
 type postgresMacroEngine struct {
-	timeRange *tsdb.TimeRange
-	query     *tsdb.Query
+	timeRange   *tsdb.TimeRange
+	query       *tsdb.Query
+	timescaledb bool
 }
 
-func newPostgresMacroEngine() tsdb.SqlMacroEngine {
-	return &postgresMacroEngine{}
+func newPostgresMacroEngine(datasource *models.DataSource) tsdb.SqlMacroEngine {
+	engine := &postgresMacroEngine{}
+	engine.timescaledb = datasource.JsonData.Get("timescaledb").MustBool(false)
+	return engine
 }
 
 func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
@@ -131,7 +135,7 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			}
 		}
 
-		if m.query.DataSource.JsonData.Get("timescaledb").MustBool() {
+		if m.timescaledb {
 			return fmt.Sprintf("time_bucket('%vs',%s)", interval.Seconds(), args[0]), nil
 		} else {
 			return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
@@ -142,7 +146,6 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 			return tg + " AS \"time\"", err
 		}
 		return "", err
-
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index 449331224c2..fe95535fe0c 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -14,10 +14,12 @@ import (
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		engine := newPostgresMacroEngine()
-		query := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}}
-		queryTS := &tsdb.Query{DataSource: &models.DataSource{JsonData: simplejson.New()}}
-		queryTS.DataSource.JsonData.Set("timescaledb", true)
+		datasource := &models.DataSource{JsonData: simplejson.New()}
+		engine := newPostgresMacroEngine(datasource)
+		datasourceTS := &models.DataSource{JsonData: simplejson.New()}
+		datasourceTS.JsonData.Set("timescaledb", true)
+		engineTS := newPostgresMacroEngine(datasourceTS)
+		query := &tsdb.Query{}
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
 			from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
@@ -89,7 +91,7 @@ func TestMacroEngine(t *testing.T) {
 
 			Convey("interpolate __timeGroup function with TimescaleDB enabled", func() {
 
-				sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+				sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
 
 				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)")
@@ -97,7 +99,7 @@ func TestMacroEngine(t *testing.T) {
 
 			Convey("interpolate __timeGroup function with spaces between args and TimescaleDB enabled", func() {
 
-				sql, err := engine.Interpolate(queryTS, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+				sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 				So(err, ShouldBeNil)
 
 				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)")
diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index b9f333db127..46d766f9a11 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -32,7 +32,7 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp
 		log: logger,
 	}
 
-	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(), logger)
+	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(datasource), logger)
 }
 
 func generateConnectionString(datasource *models.DataSource) string {
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index 87b7f916ca9..4e05f676682 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -102,7 +102,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": "SELECT * FROM postgres_types",
 								"format": "table",
@@ -183,7 +182,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
@@ -228,7 +226,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
@@ -283,7 +280,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 								"format": "time_series",
@@ -311,7 +307,6 @@ func TestPostgres(t *testing.T) {
 			query := &tsdb.TsdbQuery{
 				Queries: []*tsdb.Query{
 					{
-						DataSource: &models.DataSource{JsonData: simplejson.New()},
 						Model: simplejson.NewFromAny(map[string]interface{}{
 							"rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
 							"format": "time_series",
@@ -406,7 +401,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeInt64" as time, "timeInt64" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -429,7 +423,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeInt64Nullable" as time, "timeInt64Nullable" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -452,7 +445,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeFloat64" as time, "timeFloat64" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -475,7 +467,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeFloat64Nullable" as time, "timeFloat64Nullable" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -520,7 +511,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeInt32Nullable" as time, "timeInt32Nullable" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -543,7 +533,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeFloat32" as time, "timeFloat32" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -566,7 +555,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "timeFloat32Nullable" as time, "timeFloat32Nullable" FROM metric_values ORDER BY time LIMIT 1`,
 								"format": "time_series",
@@ -589,7 +577,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT $__timeEpoch(time), measurement || ' - value one' as metric, "valueOne" FROM metric_values ORDER BY 1`,
 								"format": "time_series",
@@ -638,7 +625,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT $__timeEpoch(time), "valueOne", "valueTwo" FROM metric_values ORDER BY 1`,
 								"format": "time_series",
@@ -696,7 +682,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`,
 								"format": "table",
@@ -720,7 +705,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`,
 								"format": "table",
@@ -747,7 +731,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": fmt.Sprintf(`SELECT
 									CAST('%s' AS TIMESTAMP) as time,
@@ -778,7 +761,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": fmt.Sprintf(`SELECT
 									 %d as time,
@@ -809,7 +791,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": fmt.Sprintf(`SELECT
 									 cast(%d as bigint) as time,
@@ -840,7 +821,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": fmt.Sprintf(`SELECT
 									 %d as time,
@@ -869,7 +849,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT
 									 cast(null as bigint) as time,
@@ -898,7 +877,6 @@ func TestPostgres(t *testing.T) {
 				query := &tsdb.TsdbQuery{
 					Queries: []*tsdb.Query{
 						{
-							DataSource: &models.DataSource{JsonData: simplejson.New()},
 							Model: simplejson.NewFromAny(map[string]interface{}{
 								"rawSql": `SELECT
 									 cast(null as timestamp) as time,

From 277a696fa577f307da16a45048261b7850e20ca7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 14 Aug 2018 08:49:56 +0200
Subject: [PATCH 316/380] fix: added missing ini default keys, fixes #12800
 (#12912)

---
 conf/defaults.ini | 3 +++
 conf/sample.ini   | 4 ++++
 2 files changed, 7 insertions(+)

diff --git a/conf/defaults.ini b/conf/defaults.ini
index b0caed81e90..99c1537eb95 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -315,6 +315,9 @@ api_url =
 team_ids =
 allowed_organizations =
 tls_skip_verify_insecure = false
+tls_client_cert =
+tls_client_key =
+tls_client_ca =
 
 #################################### Basic Auth ##########################
 [auth.basic]
diff --git a/conf/sample.ini b/conf/sample.ini
index 87544a5ac39..4291071e026 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -272,6 +272,10 @@ log_queries =
 ;api_url = https://foo.bar/user
 ;team_ids =
 ;allowed_organizations =
+;tls_skip_verify_insecure = false
+;tls_client_cert =
+;tls_client_key =
+;tls_client_ca =
 
 #################################### Grafana.com Auth ####################
 [auth.grafana_com]

From 36e808834d8aa32364663a22a977f6462567a2ae Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 08:50:22 +0200
Subject: [PATCH 317/380] don't render hidden columns in table panel (#12911)

---
 public/app/plugins/panel/table/module.html | 2 +-
 public/app/plugins/panel/table/renderer.ts | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/public/app/plugins/panel/table/module.html b/public/app/plugins/panel/table/module.html
index 5c6fcbfdb1e..e328cb09a75 100644
--- a/public/app/plugins/panel/table/module.html
+++ b/public/app/plugins/panel/table/module.html
@@ -5,7 +5,7 @@
 		<table class="table-panel-table">
 			<thead>
 				<tr>
-					<th ng-repeat="col in ctrl.table.columns" ng-hide="col.hidden">
+					<th ng-repeat="col in ctrl.table.columns" ng-if="!col.hidden">
 						<div class="table-panel-table-header-inner pointer" ng-click="ctrl.toggleColumnSort(col, $index)">
 							{{col.title}}
 							<span class="table-panel-table-header-controls" ng-if="col.sort">
diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts
index 95f54a64904..d85c20a87cc 100644
--- a/public/app/plugins/panel/table/renderer.ts
+++ b/public/app/plugins/panel/table/renderer.ts
@@ -238,6 +238,10 @@ export class TableRenderer {
       column.hidden = false;
     }
 
+    if (column.hidden === true) {
+      return '';
+    }
+
     if (column.style && column.style.preserveFormat) {
       cellClasses.push('table-panel-cell-pre');
     }

From e37931b79dc07ea19df5ab2891c2588910a22f2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 14 Aug 2018 08:52:30 +0200
Subject: [PATCH 318/380] Update CHANGELOG.md

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index efc7e44d31b..f75458820b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,8 @@
 * **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766)
+om/grafana/grafana/issues/12668)
+* **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)

From 7e0482e78d0b71872a1afed3154770922142d991 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 14 Aug 2018 08:52:51 +0200
Subject: [PATCH 319/380] Fix for Graphite function parameter quoting  (#12907)

* fix: graphite function parameters should never be quoted for boolean, node, int and float types, fixes #11927

* Update gfunc.ts
---
 .../app/plugins/datasource/graphite/gfunc.ts   |  9 ++++-----
 .../datasource/graphite/specs/gfunc.jest.ts    | 18 ++++++++++++++++++
 2 files changed, 22 insertions(+), 5 deletions(-)

diff --git a/public/app/plugins/datasource/graphite/gfunc.ts b/public/app/plugins/datasource/graphite/gfunc.ts
index 3d33d0f1005..430d0257b71 100644
--- a/public/app/plugins/datasource/graphite/gfunc.ts
+++ b/public/app/plugins/datasource/graphite/gfunc.ts
@@ -973,13 +973,12 @@ export class FuncInstance {
         } else if (_.get(_.last(this.def.params), 'multiple')) {
           paramType = _.get(_.last(this.def.params), 'type');
         }
-        if (paramType === 'value_or_series') {
+        // param types that should never be quoted
+        if (_.includes(['value_or_series', 'boolean', 'int', 'float', 'node'], paramType)) {
           return value;
         }
-        if (paramType === 'boolean' && _.includes(['true', 'false'], value)) {
-          return value;
-        }
-        if (_.includes(['int', 'float', 'int_or_interval', 'node_or_tag', 'node'], paramType) && _.isFinite(+value)) {
+        // param types that might be quoted
+        if (_.includes(['int_or_interval', 'node_or_tag'], paramType) && _.isFinite(+value)) {
           return _.toString(+value);
         }
         return "'" + value + "'";
diff --git a/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts b/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts
index feeaea2df67..08373582e73 100644
--- a/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts
+++ b/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts
@@ -55,6 +55,24 @@ describe('when rendering func instance', function() {
     expect(func.render('hello')).toEqual("movingMedian(hello, '5min')");
   });
 
+  it('should never quote boolean paramater', function() {
+    var func = gfunc.createFuncInstance('sortByName');
+    func.params[0] = '$natural';
+    expect(func.render('hello')).toEqual('sortByName(hello, $natural)');
+  });
+
+  it('should never quote int paramater', function() {
+    var func = gfunc.createFuncInstance('maximumAbove');
+    func.params[0] = '$value';
+    expect(func.render('hello')).toEqual('maximumAbove(hello, $value)');
+  });
+
+  it('should never quote node paramater', function() {
+    var func = gfunc.createFuncInstance('aliasByNode');
+    func.params[0] = '$node';
+    expect(func.render('hello')).toEqual('aliasByNode(hello, $node)');
+  });
+
   it('should handle metric param and int param and string param', function() {
     var func = gfunc.createFuncInstance('groupByNode');
     func.params[0] = 5;

From 0fa47c5ef49ba645e6945fe2d5004e84a36a5563 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 14 Aug 2018 08:55:27 +0200
Subject: [PATCH 320/380] Update CHANGELOG.md

---
 CHANGELOG.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f75458820b1..8af8027508a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,6 @@
 * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
 * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
-* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
 * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
@@ -38,6 +37,7 @@
 * **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766)
 om/grafana/grafana/issues/12668)
 * **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964)
+* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
@@ -46,6 +46,7 @@ om/grafana/grafana/issues/12668)
 * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
 * **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud)
 * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
+* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
 
 ### Breaking changes
 

From 53bab1a84bfb38e21762bdd40cdb70ca48994f4b Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 14 Aug 2018 09:15:14 +0200
Subject: [PATCH 321/380] Remove tests and logs

---
 .../panel/heatmap/HeatmapRenderContainer.tsx  |  20 -
 public/app/plugins/panel/heatmap/rendering.ts |   3 +-
 .../panel/heatmap/specs/renderer.jest.ts      | 351 ------------------
 .../panel/heatmap/specs/renderer_specs.ts     | 320 ----------------
 4 files changed, 1 insertion(+), 693 deletions(-)
 delete mode 100644 public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx
 delete mode 100644 public/app/plugins/panel/heatmap/specs/renderer.jest.ts
 delete mode 100644 public/app/plugins/panel/heatmap/specs/renderer_specs.ts

diff --git a/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx b/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx
deleted file mode 100644
index e5982a485ca..00000000000
--- a/public/app/plugins/panel/heatmap/HeatmapRenderContainer.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import HeatmapRenderer from './rendering';
-import { HeatmapCtrl } from './heatmap_ctrl';
-
-export class HeatmapRenderContainer extends React.Component {
-  renderer: any;
-  constructor(props) {
-    super(props);
-    this.renderer = HeatmapRenderer(
-      this.props.scope,
-      this.props.children[0],
-      [],
-      new HeatmapCtrl(this.props.scope, {}, {})
-    );
-  }
-
-  render() {
-    return <div />;
-  }
-}
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index 6d3d21420e0..e3318ea7e23 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/public/app/plugins/panel/heatmap/rendering.ts
@@ -154,7 +154,7 @@ export class HeatmapRenderer {
     } else {
       timeFormat = d3.timeFormat(grafanaTimeFormatter);
     }
-    console.log(ticks);
+
     let xAxis = d3
       .axisBottom(this.xScale)
       .ticks(ticks)
@@ -549,7 +549,6 @@ export class HeatmapRenderer {
       .style('opacity', this.getCardOpacity.bind(this));
 
     let $cards = this.$heatmap.find('.heatmap-card');
-    console.log($cards);
     $cards
       .on('mouseenter', event => {
         this.tooltip.mouseOverBucket = true;
diff --git a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts b/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
deleted file mode 100644
index a5546624d65..00000000000
--- a/public/app/plugins/panel/heatmap/specs/renderer.jest.ts
+++ /dev/null
@@ -1,351 +0,0 @@
-// import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
-
-import '../module';
-// import angular from 'angular';
-// import $ from 'jquery';
-// import helpers from 'test/specs/helpers';
-import TimeSeries from 'app/core/time_series2';
-import moment from 'moment';
-// import { Emitter } from 'app/core/core';
-import rendering from '../rendering';
-// import * as d3 from 'd3';
-import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter';
-jest.mock('app/core/core', () => ({
-  appEvents: {
-    on: () => {},
-  },
-  contextSrv: {
-    user: {
-      lightTheme: false,
-    },
-  },
-}));
-
-describe('grafanaHeatmap', function() {
-  //   beforeEach(angularMocks.module('grafana.core'));
-
-  let scope = <any>{};
-  let render;
-
-  function heatmapScenario(desc, func, elementWidth = 500) {
-    describe(desc, function() {
-      var ctx: any = {};
-
-      ctx.setup = function(setupFunc) {
-        // beforeEach(
-        //   angularMocks.module(function($provide) {
-        //     $provide.value('timeSrv', new helpers.TimeSrvStub());
-        //   })
-        // );
-
-        beforeEach(() => {
-          //   angularMocks.inject(function($rootScope, $compile) {
-          var ctrl: any = {
-            colorSchemes: [
-              {
-                name: 'Oranges',
-                value: 'interpolateOranges',
-                invert: 'dark',
-              },
-              { name: 'Reds', value: 'interpolateReds', invert: 'dark' },
-            ],
-            events: {
-              on: () => {},
-              emit: () => {},
-            },
-            height: 200,
-            panel: {
-              heatmap: {},
-              cards: {
-                cardPadding: null,
-                cardRound: null,
-              },
-              color: {
-                mode: 'spectrum',
-                cardColor: '#b4ff00',
-                colorScale: 'linear',
-                exponent: 0.5,
-                colorScheme: 'interpolateOranges',
-                fillBackground: false,
-              },
-              legend: {
-                show: false,
-              },
-              xBucketSize: 1000,
-              xBucketNumber: null,
-              yBucketSize: 1,
-              yBucketNumber: null,
-              xAxis: {
-                show: true,
-              },
-              yAxis: {
-                show: true,
-                format: 'short',
-                decimals: null,
-                logBase: 1,
-                splitFactor: null,
-                min: null,
-                max: null,
-                removeZeroValues: false,
-              },
-              tooltip: {
-                show: true,
-                seriesStat: false,
-                showHistogram: false,
-              },
-              highlightCards: true,
-            },
-            renderingCompleted: jest.fn(),
-            hiddenSeries: {},
-            dashboard: {
-              getTimezone: () => 'utc',
-            },
-            range: {
-              from: moment.utc('01 Mar 2017 10:00:00', 'DD MMM YYYY HH:mm:ss'),
-              to: moment.utc('01 Mar 2017 11:00:00', 'DD MMM YYYY HH:mm:ss'),
-            },
-          };
-
-          // var scope = $rootScope.$new();
-          scope.ctrl = ctrl;
-
-          ctx.series = [];
-          ctx.series.push(
-            new TimeSeries({
-              datapoints: [[1, 1422774000000], [2, 1422774060000]],
-              alias: 'series1',
-            })
-          );
-          ctx.series.push(
-            new TimeSeries({
-              datapoints: [[2, 1422774000000], [3, 1422774060000]],
-              alias: 'series2',
-            })
-          );
-
-          ctx.data = {
-            heatmapStats: {
-              min: 1,
-              max: 3,
-              minLog: 1,
-            },
-            xBucketSize: ctrl.panel.xBucketSize,
-            yBucketSize: ctrl.panel.yBucketSize,
-          };
-
-          setupFunc(ctrl, ctx);
-
-          let logBase = ctrl.panel.yAxis.logBase;
-          let bucketsData;
-          if (ctrl.panel.dataFormat === 'tsbuckets') {
-            bucketsData = histogramToHeatmap(ctx.series);
-          } else {
-            bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
-          }
-          ctx.data.buckets = bucketsData;
-
-          let { cards, cardStats } = convertToCards(bucketsData);
-          ctx.data.cards = cards;
-          ctx.data.cardStats = cardStats;
-
-          // let elemHtml = `
-          // <div class="heatmap-wrapper">
-          //   <div class="heatmap-canvas-wrapper">
-          //     <div class="heatmap-panel" style='width:${elementWidth}px'></div>
-          //   </div>
-          // </div>`;
-
-          // var element = $.parseHTML(elemHtml);
-          // $compile(element)(scope);
-          // scope.$digest();
-
-          ctrl.data = ctx.data;
-          ctx.element = {
-            find: () => ({
-              on: () => {},
-              css: () => 189,
-              width: () => 189,
-              height: () => 200,
-              find: () => ({
-                on: () => {},
-              }),
-            }),
-            on: () => {},
-          };
-          render = rendering(scope, ctx.element, [], ctrl);
-          render.render();
-          render.ctrl.renderingCompleted();
-        });
-      };
-
-      func(ctx);
-    });
-  }
-
-  heatmapScenario('default options', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-    });
-
-    it('should draw correct Y axis', function() {
-      console.log('Runnign first test');
-      // console.log(render.ctrl.data);
-      console.log(render.scope.yScale);
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).toEqual(['1', '2', '3']);
-    });
-
-    it('should draw correct X axis', function() {
-      var xTicks = getTicks(ctx.element, '.axis-x');
-      let expectedTicks = [
-        formatTime('01 Mar 2017 10:00:00'),
-        formatTime('01 Mar 2017 10:15:00'),
-        formatTime('01 Mar 2017 10:30:00'),
-        formatTime('01 Mar 2017 10:45:00'),
-        formatTime('01 Mar 2017 11:00:00'),
-      ];
-      expect(xTicks).toEqual(expectedTicks);
-    });
-  });
-
-  heatmapScenario('when logBase is 2', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 2;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).toEqual(['1', '2', '4']);
-    });
-  });
-
-  heatmapScenario('when logBase is 10', function(ctx) {
-    ctx.setup(function(ctrl, ctx) {
-      ctrl.panel.yAxis.logBase = 10;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[10, 1422774000000], [20, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 20;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).toEqual(['1', '10', '100']);
-    });
-  });
-
-  heatmapScenario('when logBase is 32', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 32;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[10, 1422774000000], [100, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 100;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).toEqual(['1', '32', '1.0 K']);
-    });
-  });
-
-  heatmapScenario('when logBase is 1024', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1024;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[2000, 1422774000000], [300000, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 300000;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).toEqual(['1', '1 K', '1.0 Mil']);
-    });
-  });
-
-  heatmapScenario('when Y axis format set to "none"', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-      ctrl.panel.yAxis.format = 'none';
-      ctx.data.heatmapStats.max = 10000;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).toEqual(['0', '2000', '4000', '6000', '8000', '10000', '12000']);
-    });
-  });
-
-  heatmapScenario('when Y axis format set to "second"', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-      ctrl.panel.yAxis.format = 's';
-      ctx.data.heatmapStats.max = 3600;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).toEqual(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']);
-    });
-  });
-
-  heatmapScenario('when data format is Time series buckets', function(ctx) {
-    ctx.setup(function(ctrl, ctx) {
-      ctrl.panel.dataFormat = 'tsbuckets';
-
-      const series = [
-        {
-          alias: '1',
-          datapoints: [[1000, 1422774000000], [200000, 1422774060000]],
-        },
-        {
-          alias: '2',
-          datapoints: [[3000, 1422774000000], [400000, 1422774060000]],
-        },
-        {
-          alias: '3',
-          datapoints: [[2000, 1422774000000], [300000, 1422774060000]],
-        },
-      ];
-      ctx.series = series.map(s => new TimeSeries(s));
-
-      ctx.data.tsBuckets = series.map(s => s.alias).concat('');
-      ctx.data.yBucketSize = 1;
-      let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]);
-      ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet);
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).toEqual(['1', '2', '3', '']);
-    });
-  });
-});
-
-function getTicks(element, axisSelector) {
-  // return element
-  //   .find(axisSelector)
-  //   .find('text')
-  //   .map(function() {
-  //     return this.textContent;
-  //   })
-  //   .get();
-}
-
-function formatTime(timeStr) {
-  let format = 'HH:mm';
-  return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format);
-}
diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts
deleted file mode 100644
index f52b6d1d985..00000000000
--- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts
+++ /dev/null
@@ -1,320 +0,0 @@
-import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
-
-import '../module';
-import angular from 'angular';
-import $ from 'jquery';
-import helpers from 'test/specs/helpers';
-import TimeSeries from 'app/core/time_series2';
-import moment from 'moment';
-import { Emitter } from 'app/core/core';
-import rendering from '../rendering';
-import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter';
-
-describe('grafanaHeatmap', function() {
-  beforeEach(angularMocks.module('grafana.core'));
-
-  function heatmapScenario(desc, func, elementWidth = 500) {
-    describe(desc, function() {
-      var ctx: any = {};
-
-      ctx.setup = function(setupFunc) {
-        beforeEach(
-          angularMocks.module(function($provide) {
-            $provide.value('timeSrv', new helpers.TimeSrvStub());
-          })
-        );
-
-        beforeEach(
-          angularMocks.inject(function($rootScope, $compile) {
-            var ctrl: any = {
-              colorSchemes: [
-                {
-                  name: 'Oranges',
-                  value: 'interpolateOranges',
-                  invert: 'dark',
-                },
-                { name: 'Reds', value: 'interpolateReds', invert: 'dark' },
-              ],
-              events: new Emitter(),
-              height: 200,
-              panel: {
-                heatmap: {},
-                cards: {
-                  cardPadding: null,
-                  cardRound: null,
-                },
-                color: {
-                  mode: 'spectrum',
-                  cardColor: '#b4ff00',
-                  colorScale: 'linear',
-                  exponent: 0.5,
-                  colorScheme: 'interpolateOranges',
-                  fillBackground: false,
-                },
-                legend: {
-                  show: false,
-                },
-                xBucketSize: 1000,
-                xBucketNumber: null,
-                yBucketSize: 1,
-                yBucketNumber: null,
-                xAxis: {
-                  show: true,
-                },
-                yAxis: {
-                  show: true,
-                  format: 'short',
-                  decimals: null,
-                  logBase: 1,
-                  splitFactor: null,
-                  min: null,
-                  max: null,
-                  removeZeroValues: false,
-                },
-                tooltip: {
-                  show: true,
-                  seriesStat: false,
-                  showHistogram: false,
-                },
-                highlightCards: true,
-              },
-              renderingCompleted: sinon.spy(),
-              hiddenSeries: {},
-              dashboard: {
-                getTimezone: sinon.stub().returns('utc'),
-              },
-              range: {
-                from: moment.utc('01 Mar 2017 10:00:00', 'DD MMM YYYY HH:mm:ss'),
-                to: moment.utc('01 Mar 2017 11:00:00', 'DD MMM YYYY HH:mm:ss'),
-              },
-            };
-
-            var scope = $rootScope.$new();
-            scope.ctrl = ctrl;
-
-            ctx.series = [];
-            ctx.series.push(
-              new TimeSeries({
-                datapoints: [[1, 1422774000000], [2, 1422774060000]],
-                alias: 'series1',
-              })
-            );
-            ctx.series.push(
-              new TimeSeries({
-                datapoints: [[2, 1422774000000], [3, 1422774060000]],
-                alias: 'series2',
-              })
-            );
-
-            ctx.data = {
-              heatmapStats: {
-                min: 1,
-                max: 3,
-                minLog: 1,
-              },
-              xBucketSize: ctrl.panel.xBucketSize,
-              yBucketSize: ctrl.panel.yBucketSize,
-            };
-
-            setupFunc(ctrl, ctx);
-
-            let logBase = ctrl.panel.yAxis.logBase;
-            let bucketsData;
-            if (ctrl.panel.dataFormat === 'tsbuckets') {
-              bucketsData = histogramToHeatmap(ctx.series);
-            } else {
-              bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
-            }
-            ctx.data.buckets = bucketsData;
-
-            let { cards, cardStats } = convertToCards(bucketsData);
-            ctx.data.cards = cards;
-            ctx.data.cardStats = cardStats;
-
-            let elemHtml = `
-          <div class="heatmap-wrapper">
-            <div class="heatmap-canvas-wrapper">
-              <div class="heatmap-panel" style='width:${elementWidth}px'></div>
-            </div>
-          </div>`;
-
-            var element = angular.element(elemHtml);
-            $compile(element)(scope);
-            scope.$digest();
-
-            ctrl.data = ctx.data;
-            ctx.element = element;
-            rendering(scope, $(element), [], ctrl);
-            ctrl.events.emit('render');
-          })
-        );
-      };
-
-      func(ctx);
-    });
-  }
-
-  heatmapScenario('default options', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '2', '3']);
-    });
-
-    it('should draw correct X axis', function() {
-      var xTicks = getTicks(ctx.element, '.axis-x');
-      let expectedTicks = [
-        formatTime('01 Mar 2017 10:00:00'),
-        formatTime('01 Mar 2017 10:15:00'),
-        formatTime('01 Mar 2017 10:30:00'),
-        formatTime('01 Mar 2017 10:45:00'),
-        formatTime('01 Mar 2017 11:00:00'),
-      ];
-      expect(xTicks).to.eql(expectedTicks);
-    });
-  });
-
-  heatmapScenario('when logBase is 2', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 2;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '2', '4']);
-    });
-  });
-
-  heatmapScenario('when logBase is 10', function(ctx) {
-    ctx.setup(function(ctrl, ctx) {
-      ctrl.panel.yAxis.logBase = 10;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[10, 1422774000000], [20, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 20;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '10', '100']);
-    });
-  });
-
-  heatmapScenario('when logBase is 32', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 32;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[10, 1422774000000], [100, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 100;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '32', '1.0 K']);
-    });
-  });
-
-  heatmapScenario('when logBase is 1024', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1024;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[2000, 1422774000000], [300000, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 300000;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '1 K', '1.0 Mil']);
-    });
-  });
-
-  heatmapScenario('when Y axis format set to "none"', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-      ctrl.panel.yAxis.format = 'none';
-      ctx.data.heatmapStats.max = 10000;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['0', '2000', '4000', '6000', '8000', '10000', '12000']);
-    });
-  });
-
-  heatmapScenario('when Y axis format set to "second"', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-      ctrl.panel.yAxis.format = 's';
-      ctx.data.heatmapStats.max = 3600;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']);
-    });
-  });
-
-  heatmapScenario('when data format is Time series buckets', function(ctx) {
-    ctx.setup(function(ctrl, ctx) {
-      ctrl.panel.dataFormat = 'tsbuckets';
-
-      const series = [
-        {
-          alias: '1',
-          datapoints: [[1000, 1422774000000], [200000, 1422774060000]],
-        },
-        {
-          alias: '2',
-          datapoints: [[3000, 1422774000000], [400000, 1422774060000]],
-        },
-        {
-          alias: '3',
-          datapoints: [[2000, 1422774000000], [300000, 1422774060000]],
-        },
-      ];
-      ctx.series = series.map(s => new TimeSeries(s));
-
-      ctx.data.tsBuckets = series.map(s => s.alias).concat('');
-      ctx.data.yBucketSize = 1;
-      let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]);
-      ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet);
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '2', '3', '']);
-    });
-  });
-});
-
-function getTicks(element, axisSelector) {
-  return element
-    .find(axisSelector)
-    .find('text')
-    .map(function() {
-      return this.textContent;
-    })
-    .get();
-}
-
-function formatTime(timeStr) {
-  let format = 'HH:mm';
-  return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format);
-}

From 3955133f7e143002bd7b141808a1323ade444694 Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 14 Aug 2018 09:15:24 +0200
Subject: [PATCH 322/380] Don't pass datasource to newPostgresMacroEngine

---
 pkg/tsdb/postgres/macros.go      | 7 ++-----
 pkg/tsdb/postgres/macros_test.go | 9 ++-------
 pkg/tsdb/postgres/postgres.go    | 4 +++-
 3 files changed, 7 insertions(+), 13 deletions(-)

diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 81b0da9fbce..0a9162a2d4c 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -7,7 +7,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 
@@ -21,10 +20,8 @@ type postgresMacroEngine struct {
 	timescaledb bool
 }
 
-func newPostgresMacroEngine(datasource *models.DataSource) tsdb.SqlMacroEngine {
-	engine := &postgresMacroEngine{}
-	engine.timescaledb = datasource.JsonData.Get("timescaledb").MustBool(false)
-	return engine
+func newPostgresMacroEngine(timescaledb bool) tsdb.SqlMacroEngine {
+	return &postgresMacroEngine{timescaledb: timescaledb}
 }
 
 func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index fe95535fe0c..30a57a7095f 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -6,19 +6,14 @@ import (
 	"testing"
 	"time"
 
-	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		datasource := &models.DataSource{JsonData: simplejson.New()}
-		engine := newPostgresMacroEngine(datasource)
-		datasourceTS := &models.DataSource{JsonData: simplejson.New()}
-		datasourceTS.JsonData.Set("timescaledb", true)
-		engineTS := newPostgresMacroEngine(datasourceTS)
+		engine := newPostgresMacroEngine(false)
+		engineTS := newPostgresMacroEngine(true)
 		query := &tsdb.Query{}
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index 46d766f9a11..4bcf06638f4 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -32,7 +32,9 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp
 		log: logger,
 	}
 
-	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(datasource), logger)
+	timescaledb := datasource.JsonData.Get("timescaledb").MustBool(false)
+
+	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(timescaledb), logger)
 }
 
 func generateConnectionString(datasource *models.DataSource) string {

From 4f704cec532529542dbc8c1912e666e168d4b36e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 14 Aug 2018 09:18:04 +0200
Subject: [PATCH 323/380] fix: ds_proxy test not initiating header

---
 pkg/api/pluginproxy/ds_proxy_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go
index bb553b4d075..9b768c3d32a 100644
--- a/pkg/api/pluginproxy/ds_proxy_test.go
+++ b/pkg/api/pluginproxy/ds_proxy_test.go
@@ -219,7 +219,7 @@ func TestDSRouteRule(t *testing.T) {
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
 
 			requestURL, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestURL}
+			req := http.Request{URL: requestURL, Header: http.Header{}}
 
 			proxy.getDirector()(&req)
 
@@ -244,7 +244,7 @@ func TestDSRouteRule(t *testing.T) {
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 
 			requestURL, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestURL}
+			req := http.Request{URL: requestURL, Header: http.Header{}}
 
 			proxy.getDirector()(&req)
 

From 766d0bef17fde119ffddbc827177e2c3f4d36fe3 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 09:19:37 +0200
Subject: [PATCH 324/380] changelog: add notes about closing #10705

[skip ci]
---
 CHANGELOG.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8af8027508a..7cd75402946 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
 
 * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
+* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)
 * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
@@ -27,7 +28,6 @@
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 * **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
-* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88)
@@ -41,6 +41,7 @@ om/grafana/grafana/issues/12668)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
+* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
 * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
 * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)

From e696dc4d5f895d0c17ff3e02ac2c9181d2b234ab Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 14 Aug 2018 09:28:08 +0200
Subject: [PATCH 325/380] Remove Karma scripts and docs

---
 .github/CONTRIBUTING.md                      |  6 ++++-
 README.md                                    | 12 ++--------
 docs/sources/project/building_from_source.md |  8 +++----
 package.json                                 | 10 ---------
 scripts/grunt/default_task.js                |  1 -
 scripts/grunt/options/karma.js               | 23 --------------------
 6 files changed, 10 insertions(+), 50 deletions(-)
 delete mode 100644 scripts/grunt/options/karma.js

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index fe0a1d6c548..f0f4e19bfc3 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -7,7 +7,11 @@ grunt && grunt watch
 
 ### Rerun tests on source change
 ```
-grunt karma:dev
+npm jest
+```
+or
+```
+yarn jest
 ```
 
 ### Run tests for backend assets before commit
diff --git a/README.md b/README.md
index d6083bb1504..71fdb04cea6 100644
--- a/README.md
+++ b/README.md
@@ -59,11 +59,6 @@ Run tests
 yarn run jest
 ```
 
-Run karma tests
-```bash
-yarn run karma
-```
-
 ### Recompile backend on source change
 
 To rebuild on source change.
@@ -101,14 +96,11 @@ Execute all frontend tests
 yarn run test
 ```
 
-Writing & watching frontend tests (we have two test runners)
+Writing & watching frontend tests
 
 - jest for all new tests that do not require browser context (React+more)
    - Start watcher: `yarn run jest`
-   - Jest will run all test files that end with the name ".jest.ts"
-- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
-  - Start watcher: `yarn run karma`
-  - Karma+Mocha runs all files that end with the name "_specs.ts".
+   - Jest will run all test files that end with the name ".test.ts"
 
 #### Backend
 ```bash
diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md
index a0b553594ce..20c177211e3 100644
--- a/docs/sources/project/building_from_source.md
+++ b/docs/sources/project/building_from_source.md
@@ -90,14 +90,12 @@ You'll also need to run `npm run watch` to watch for changes to the front-end (t
 - You can run backend Golang tests using "go test ./pkg/...".
 - Execute all frontend tests with "npm run test"
 
-Writing & watching frontend tests (we have two test runners)
+Writing & watching frontend tests
 
 - jest for all new tests that do not require browser context (React+more)
    - Start watcher: `npm run jest`
-   - Jest will run all test files that end with the name ".jest.ts"
-- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
-  - Start watcher: `npm run karma`
-  - Karma+Mocha runs all files that end with the name "_specs.ts".
+   - Jest will run all test files that end with the name ".test.ts"
+
 
 ## Creating optimized release packages
 
diff --git a/package.json b/package.json
index 24e23b574df..87615e8273b 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,6 @@
     "grunt-contrib-copy": "~1.0.0",
     "grunt-contrib-cssmin": "~1.0.2",
     "grunt-exec": "^1.0.1",
-    "grunt-karma": "~2.0.0",
     "grunt-notify": "^0.4.5",
     "grunt-postcss": "^0.8.0",
     "grunt-sass": "^2.0.0",
@@ -58,14 +57,6 @@
     "html-webpack-plugin": "^3.2.0",
     "husky": "^0.14.3",
     "jest": "^22.0.4",
-    "karma": "1.7.0",
-    "karma-chrome-launcher": "~2.2.0",
-    "karma-expect": "~1.1.3",
-    "karma-mocha": "~1.3.0",
-    "karma-phantomjs-launcher": "1.0.4",
-    "karma-sinon": "^1.0.5",
-    "karma-sourcemap-loader": "^0.3.7",
-    "karma-webpack": "^3.0.0",
     "lint-staged": "^6.0.0",
     "load-grunt-tasks": "3.5.2",
     "mini-css-extract-plugin": "^0.4.0",
@@ -112,7 +103,6 @@
     "test": "grunt test",
     "test:coverage": "grunt test --coverage=true",
     "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
-    "karma": "grunt karma:dev",
     "jest": "jest --notify --watch",
     "api-tests": "jest --notify --watch --config=tests/api/jest.js",
     "precommit": "lint-staged && grunt precommit"
diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js
index efcdcd02963..07519cdd6c8 100644
--- a/scripts/grunt/default_task.js
+++ b/scripts/grunt/default_task.js
@@ -12,7 +12,6 @@ module.exports = function(grunt) {
     'sasslint',
     'exec:tslint',
     "exec:jest",
-    'karma:test',
     'no-only-tests'
   ]);
 
diff --git a/scripts/grunt/options/karma.js b/scripts/grunt/options/karma.js
deleted file mode 100644
index 9f638d2e36d..00000000000
--- a/scripts/grunt/options/karma.js
+++ /dev/null
@@ -1,23 +0,0 @@
-module.exports = function (config) {
-  'use strict';
-
-  return {
-    dev: {
-      configFile: 'karma.conf.js',
-      singleRun: false,
-    },
-
-    debug: {
-      configFile: 'karma.conf.js',
-      singleRun: false,
-      browsers: ['Chrome'],
-      mime: {
-        'text/x-typescript': ['ts', 'tsx']
-      },
-    },
-
-    test: {
-      configFile: 'karma.conf.js',
-    }
-  };
-};

From 837388d13e0a0a84c4829edf4eca285321079f5e Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 14 Aug 2018 09:44:58 +0200
Subject: [PATCH 326/380] Use variable in newPostgresMacroEngine

---
 pkg/tsdb/postgres/macros_test.go | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index 30a57a7095f..f0c8832dd05 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -12,8 +12,10 @@ import (
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		engine := newPostgresMacroEngine(false)
-		engineTS := newPostgresMacroEngine(true)
+		timescaledbEnabled := false
+		engine := newPostgresMacroEngine(timescaledbEnabled)
+		timescaledbEnabled = true
+		engineTS := newPostgresMacroEngine(timescaledbEnabled)
 		query := &tsdb.Query{}
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {

From d33019ca6740e55d89119bbf6fce32056cdded3f Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 14 Aug 2018 10:22:57 +0200
Subject: [PATCH 327/380] document TimescaleDB datasource option

---
 docs/sources/features/datasources/postgres.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index 2be2db0837b..e8ed742f64f 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -31,6 +31,7 @@ Name | Description
 *User* | Database user's login/username
 *Password* | Database user's password
 *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
+*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time.
 
 ### Database User Permissions (Important!)
 

From a96d97e347ad8a8725ca7f8fbb80271812d7e64c Mon Sep 17 00:00:00 2001
From: Sven Klemm <sven@timescale.com>
Date: Tue, 14 Aug 2018 10:26:08 +0200
Subject: [PATCH 328/380] add version disclaimer for TimescaleDB

---
 docs/sources/features/datasources/postgres.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index e8ed742f64f..e2dcf888025 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -31,7 +31,7 @@ Name | Description
 *User* | Database user's login/username
 *Password* | Database user's password
 *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
-*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time.
+*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+).
 
 ### Database User Permissions (Important!)
 

From b70d594c103de35875600cddabe9130468435cb6 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 10:35:34 +0200
Subject: [PATCH 329/380] changelog: add notes about closing #12598

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cd75402946..0c397e45ea4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,7 @@ om/grafana/grafana/issues/12668)
 * **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud)
 * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
 * **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
+* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
 
 ### Breaking changes
 

From a65589a5fbeb2fde7e5cc2dd6613fb8bf0355ae5 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 14 Aug 2018 10:52:41 +0200
Subject: [PATCH 330/380] Rename test files

---
 .github/CONTRIBUTING.md                       |  2 +-
 jest.config.js                                |  2 +-
 karma.conf.js                                 | 40 -------------------
 ...leList.jest.tsx => AlertRuleList.test.tsx} |  0
 ...t.tsx.snap => AlertRuleList.test.tsx.snap} |  0
 ...Field.jest.tsx => PromQueryField.test.tsx} |  0
 ...imePicker.jest.tsx => TimePicker.test.tsx} |  0
 .../{braces.jest.ts => braces.test.ts}        |  0
 .../{clear.jest.ts => clear.test.ts}          |  0
 ...{prometheus.jest.ts => prometheus.test.ts} |  0
 ...tings.jest.tsx => FolderSettings.test.tsx} |  0
 ...verStats.jest.tsx => ServerStats.test.tsx} |  0
 ...est.tsx.snap => ServerStats.test.tsx.snap} |  0
 ...eButton.jest.tsx => DeleteButton.test.tsx} |  0
 ...ListCTA.jest.tsx => EmptyListCTA.test.tsx} |  0
 ...st.tsx.snap => EmptyListCTA.test.tsx.snap} |  0
 ...ageHeader.jest.tsx => PageHeader.test.tsx} |  0
 ...sions.jest.tsx => AddPermissions.test.tsx} |  0
 ...rOption.jest.tsx => PickerOption.test.tsx} |  0
 ...eamPicker.jest.tsx => TeamPicker.test.tsx} |  0
 ...serPicker.jest.tsx => UserPicker.test.tsx} |  0
 ...st.tsx.snap => PickerOption.test.tsx.snap} |  0
 ...jest.tsx.snap => TeamPicker.test.tsx.snap} |  0
 ...jest.tsx.snap => UserPicker.test.tsx.snap} |  0
 .../{Popover.jest.tsx => Popover.test.tsx}    |  0
 .../{Tooltip.jest.tsx => Tooltip.test.tsx}    |  0
 ...er.jest.tsx.snap => Popover.test.tsx.snap} |  0
 ...ip.jest.tsx.snap => Tooltip.test.tsx.snap} |  0
 ...Palette.jest.tsx => ColorPalette.test.tsx} |  0
 ...gth.jest.tsx => PasswordStrength.test.tsx} |  0
 ...st.tsx.snap => ColorPalette.test.tsx.snap} |  0
 ...ackend_srv.jest.ts => backend_srv.test.ts} |  0
 .../{datemath.jest.ts => datemath.test.ts}    |  0
 .../{emitter.jest.ts => emitter.test.ts}      |  0
 ...ile_export.jest.ts => file_export.test.ts} |  0
 .../{flatten.jest.ts => flatten.test.ts}      |  0
 .../core/specs/{kbn.jest.ts => kbn.test.ts}   |  0
 ...ion_util.jest.ts => location_util.test.ts} |  0
 ...ards.jest.ts => manage_dashboards.test.ts} |  0
 ..._switcher.jest.ts => org_switcher.test.ts} |  0
 .../{rangeutil.jest.ts => rangeutil.test.ts}  |  0
 .../specs/{search.jest.ts => search.test.ts}  |  0
 ...results.jest.ts => search_results.test.ts} |  0
 ...{search_srv.jest.ts => search_srv.test.ts} |  0
 .../specs/{store.jest.ts => store.test.ts}    |  0
 ...able_model.jest.ts => table_model.test.ts} |  0
 .../specs/{ticks.jest.ts => ticks.test.ts}    |  0
 ...ime_series.jest.ts => time_series.test.ts} |  0
 ....jest.ts => value_select_dropdown.test.ts} |  0
 ...apper.jest.ts => threshold_mapper.test.ts} |  0
 ...ns_srv.jest.ts => annotations_srv.test.ts} |  0
 ....jest.ts => annotations_srv_specs.test.ts} |  0
 ...lPanel.jest.tsx => AddPanelPanel.test.tsx} |  0
 ...oardRow.jest.tsx => DashboardRow.test.tsx} |  0
 ...tracker.jest.ts => change_tracker.test.ts} |  0
 ....jest.ts => dashboard_import_ctrl.test.ts} |  0
 ...on.jest.ts => dashboard_migration.test.ts} |  0
 ..._model.jest.ts => dashboard_model.test.ts} |  0
 .../{exporter.jest.ts => exporter.test.ts}    |  0
 ...tory_ctrl.jest.ts => history_ctrl.test.ts} |  0
 ...istory_srv.jest.ts => history_srv.test.ts} |  0
 .../specs/{repeat.jest.ts => repeat.test.ts}  |  0
 ...as_modal.jest.ts => save_as_modal.test.ts} |  0
 ...{save_modal.jest.ts => save_modal.test.ts} |  0
 ...jest.ts => save_provisioned_modal.test.ts} |  0
 .../{time_srv.jest.ts => time_srv.test.ts}    |  0
 ...tate_srv.jest.ts => viewstate_srv.test.ts} |  0
 ...trl.jest.ts => metrics_panel_ctrl.test.ts} |  0
 .../{link_srv.jest.ts => link_srv.test.ts}    |  0
 ...trl.jest.ts => playlist_edit_ctrl.test.ts} |  0
 ...rce_srv.jest.ts => datasource_srv.test.ts} |  0
 ...ariable.jest.ts => adhoc_variable.test.ts} |  0
 ...ditor_ctrl.jest.ts => editor_ctrl.test.ts} |  0
 ...ariable.jest.ts => query_variable.test.ts} |  0
 ...plate_srv.jest.ts => template_srv.test.ts} |  0
 .../{variable.jest.ts => variable.test.ts}    |  0
 ...iable_srv.jest.ts => variable_srv.test.ts} |  0
 ...init.jest.ts => variable_srv_init.test.ts} |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 ...ponse.jest.ts => elastic_response.test.ts} |  0
 ..._pattern.jest.ts => index_pattern.test.ts} |  0
 ..._builder.jest.ts => query_builder.test.ts} |  0
 .../{query_def.jest.ts => query_def.test.ts}  |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 .../specs/{gfunc.jest.ts => gfunc.test.ts}    |  0
 ...e_query.jest.ts => graphite_query.test.ts} |  0
 .../specs/{lexer.jest.ts => lexer.test.ts}    |  0
 .../specs/{parser.jest.ts => parser.test.ts}  |  0
 ...{query_ctrl.jest.ts => query_ctrl.test.ts} |  0
 ...lux_query.jest.ts => influx_query.test.ts} |  0
 ...x_series.jest.ts => influx_series.test.ts} |  0
 ..._builder.jest.ts => query_builder.test.ts} |  0
 ...{query_ctrl.jest.ts => query_ctrl.test.ts} |  0
 ...{query_part.jest.ts => query_part.test.ts} |  0
 ...parser.jest.ts => response_parser.test.ts} |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 ...mer.jest.ts => result_transformer.test.ts} |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 ...{query_ctrl.jest.ts => query_ctrl.test.ts} |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 .../{completer.jest.ts => completer.test.ts}  |  0
 ...{datasource.jest.ts => datasource.test.ts} |  0
 ...uery.jest.ts => metric_find_query.test.ts} |  0
 ...mer.jest.ts => result_transformer.test.ts} |  0
 ...lign_yaxes.jest.ts => align_yaxes.test.ts} |  0
 ...ocessor.jest.ts => data_processor.test.ts} |  0
 .../specs/{graph.jest.ts => graph.test.ts}    |  0
 ...{graph_ctrl.jest.ts => graph_ctrl.test.ts} |  0
 ..._tooltip.jest.ts => graph_tooltip.test.ts} |  0
 .../{histogram.jest.ts => histogram.test.ts}  |  0
 ...l.jest.ts => series_override_ctrl.test.ts} |  0
 ...ager.jest.ts => threshold_manager.test.ts} |  0
 ...tmap_ctrl.jest.ts => heatmap_ctrl.test.ts} |  0
 ...jest.ts => heatmap_data_converter.test.ts} |  0
 ...{singlestat.jest.ts => singlestat.test.ts} |  0
 ...panel.jest.ts => singlestat_panel.test.ts} |  0
 .../{renderer.jest.ts => renderer.test.ts}    |  0
 ...nsformers.jest.ts => transformers.test.ts} |  0
 ...stStore.jest.ts => AlertListStore.test.ts} |  0
 .../{NavStore.jest.ts => NavStore.test.ts}    |  0
 ...Store.jest.ts => PermissionsStore.test.ts} |  0
 .../{ViewStore.jest.ts => ViewStore.test.ts}  |  0
 .../{version_jest.ts => version_test.ts}      |  0
 126 files changed, 2 insertions(+), 42 deletions(-)
 delete mode 100644 karma.conf.js
 rename public/app/containers/AlertRuleList/{AlertRuleList.jest.tsx => AlertRuleList.test.tsx} (100%)
 rename public/app/containers/AlertRuleList/__snapshots__/{AlertRuleList.jest.tsx.snap => AlertRuleList.test.tsx.snap} (100%)
 rename public/app/containers/Explore/{PromQueryField.jest.tsx => PromQueryField.test.tsx} (100%)
 rename public/app/containers/Explore/{TimePicker.jest.tsx => TimePicker.test.tsx} (100%)
 rename public/app/containers/Explore/slate-plugins/{braces.jest.ts => braces.test.ts} (100%)
 rename public/app/containers/Explore/slate-plugins/{clear.jest.ts => clear.test.ts} (100%)
 rename public/app/containers/Explore/utils/{prometheus.jest.ts => prometheus.test.ts} (100%)
 rename public/app/containers/ManageDashboards/{FolderSettings.jest.tsx => FolderSettings.test.tsx} (100%)
 rename public/app/containers/ServerStats/{ServerStats.jest.tsx => ServerStats.test.tsx} (100%)
 rename public/app/containers/ServerStats/__snapshots__/{ServerStats.jest.tsx.snap => ServerStats.test.tsx.snap} (100%)
 rename public/app/core/components/DeleteButton/{DeleteButton.jest.tsx => DeleteButton.test.tsx} (100%)
 rename public/app/core/components/EmptyListCTA/{EmptyListCTA.jest.tsx => EmptyListCTA.test.tsx} (100%)
 rename public/app/core/components/EmptyListCTA/__snapshots__/{EmptyListCTA.jest.tsx.snap => EmptyListCTA.test.tsx.snap} (100%)
 rename public/app/core/components/PageHeader/{PageHeader.jest.tsx => PageHeader.test.tsx} (100%)
 rename public/app/core/components/Permissions/{AddPermissions.jest.tsx => AddPermissions.test.tsx} (100%)
 rename public/app/core/components/Picker/{PickerOption.jest.tsx => PickerOption.test.tsx} (100%)
 rename public/app/core/components/Picker/{TeamPicker.jest.tsx => TeamPicker.test.tsx} (100%)
 rename public/app/core/components/Picker/{UserPicker.jest.tsx => UserPicker.test.tsx} (100%)
 rename public/app/core/components/Picker/__snapshots__/{PickerOption.jest.tsx.snap => PickerOption.test.tsx.snap} (100%)
 rename public/app/core/components/Picker/__snapshots__/{TeamPicker.jest.tsx.snap => TeamPicker.test.tsx.snap} (100%)
 rename public/app/core/components/Picker/__snapshots__/{UserPicker.jest.tsx.snap => UserPicker.test.tsx.snap} (100%)
 rename public/app/core/components/Tooltip/{Popover.jest.tsx => Popover.test.tsx} (100%)
 rename public/app/core/components/Tooltip/{Tooltip.jest.tsx => Tooltip.test.tsx} (100%)
 rename public/app/core/components/Tooltip/__snapshots__/{Popover.jest.tsx.snap => Popover.test.tsx.snap} (100%)
 rename public/app/core/components/Tooltip/__snapshots__/{Tooltip.jest.tsx.snap => Tooltip.test.tsx.snap} (100%)
 rename public/app/core/specs/{ColorPalette.jest.tsx => ColorPalette.test.tsx} (100%)
 rename public/app/core/specs/{PasswordStrength.jest.tsx => PasswordStrength.test.tsx} (100%)
 rename public/app/core/specs/__snapshots__/{ColorPalette.jest.tsx.snap => ColorPalette.test.tsx.snap} (100%)
 rename public/app/core/specs/{backend_srv.jest.ts => backend_srv.test.ts} (100%)
 rename public/app/core/specs/{datemath.jest.ts => datemath.test.ts} (100%)
 rename public/app/core/specs/{emitter.jest.ts => emitter.test.ts} (100%)
 rename public/app/core/specs/{file_export.jest.ts => file_export.test.ts} (100%)
 rename public/app/core/specs/{flatten.jest.ts => flatten.test.ts} (100%)
 rename public/app/core/specs/{kbn.jest.ts => kbn.test.ts} (100%)
 rename public/app/core/specs/{location_util.jest.ts => location_util.test.ts} (100%)
 rename public/app/core/specs/{manage_dashboards.jest.ts => manage_dashboards.test.ts} (100%)
 rename public/app/core/specs/{org_switcher.jest.ts => org_switcher.test.ts} (100%)
 rename public/app/core/specs/{rangeutil.jest.ts => rangeutil.test.ts} (100%)
 rename public/app/core/specs/{search.jest.ts => search.test.ts} (100%)
 rename public/app/core/specs/{search_results.jest.ts => search_results.test.ts} (100%)
 rename public/app/core/specs/{search_srv.jest.ts => search_srv.test.ts} (100%)
 rename public/app/core/specs/{store.jest.ts => store.test.ts} (100%)
 rename public/app/core/specs/{table_model.jest.ts => table_model.test.ts} (100%)
 rename public/app/core/specs/{ticks.jest.ts => ticks.test.ts} (100%)
 rename public/app/core/specs/{time_series.jest.ts => time_series.test.ts} (100%)
 rename public/app/core/specs/{value_select_dropdown.jest.ts => value_select_dropdown.test.ts} (100%)
 rename public/app/features/alerting/specs/{threshold_mapper.jest.ts => threshold_mapper.test.ts} (100%)
 rename public/app/features/annotations/specs/{annotations_srv.jest.ts => annotations_srv.test.ts} (100%)
 rename public/app/features/annotations/specs/{annotations_srv_specs.jest.ts => annotations_srv_specs.test.ts} (100%)
 rename public/app/features/dashboard/specs/{AddPanelPanel.jest.tsx => AddPanelPanel.test.tsx} (100%)
 rename public/app/features/dashboard/specs/{DashboardRow.jest.tsx => DashboardRow.test.tsx} (100%)
 rename public/app/features/dashboard/specs/{change_tracker.jest.ts => change_tracker.test.ts} (100%)
 rename public/app/features/dashboard/specs/{dashboard_import_ctrl.jest.ts => dashboard_import_ctrl.test.ts} (100%)
 rename public/app/features/dashboard/specs/{dashboard_migration.jest.ts => dashboard_migration.test.ts} (100%)
 rename public/app/features/dashboard/specs/{dashboard_model.jest.ts => dashboard_model.test.ts} (100%)
 rename public/app/features/dashboard/specs/{exporter.jest.ts => exporter.test.ts} (100%)
 rename public/app/features/dashboard/specs/{history_ctrl.jest.ts => history_ctrl.test.ts} (100%)
 rename public/app/features/dashboard/specs/{history_srv.jest.ts => history_srv.test.ts} (100%)
 rename public/app/features/dashboard/specs/{repeat.jest.ts => repeat.test.ts} (100%)
 rename public/app/features/dashboard/specs/{save_as_modal.jest.ts => save_as_modal.test.ts} (100%)
 rename public/app/features/dashboard/specs/{save_modal.jest.ts => save_modal.test.ts} (100%)
 rename public/app/features/dashboard/specs/{save_provisioned_modal.jest.ts => save_provisioned_modal.test.ts} (100%)
 rename public/app/features/dashboard/specs/{time_srv.jest.ts => time_srv.test.ts} (100%)
 rename public/app/features/dashboard/specs/{viewstate_srv.jest.ts => viewstate_srv.test.ts} (100%)
 rename public/app/features/panel/specs/{metrics_panel_ctrl.jest.ts => metrics_panel_ctrl.test.ts} (100%)
 rename public/app/features/panellinks/specs/{link_srv.jest.ts => link_srv.test.ts} (100%)
 rename public/app/features/playlist/specs/{playlist_edit_ctrl.jest.ts => playlist_edit_ctrl.test.ts} (100%)
 rename public/app/features/plugins/specs/{datasource_srv.jest.ts => datasource_srv.test.ts} (100%)
 rename public/app/features/templating/specs/{adhoc_variable.jest.ts => adhoc_variable.test.ts} (100%)
 rename public/app/features/templating/specs/{editor_ctrl.jest.ts => editor_ctrl.test.ts} (100%)
 rename public/app/features/templating/specs/{query_variable.jest.ts => query_variable.test.ts} (100%)
 rename public/app/features/templating/specs/{template_srv.jest.ts => template_srv.test.ts} (100%)
 rename public/app/features/templating/specs/{variable.jest.ts => variable.test.ts} (100%)
 rename public/app/features/templating/specs/{variable_srv.jest.ts => variable_srv.test.ts} (100%)
 rename public/app/features/templating/specs/{variable_srv_init.jest.ts => variable_srv_init.test.ts} (100%)
 rename public/app/plugins/datasource/cloudwatch/specs/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/elasticsearch/specs/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/elasticsearch/specs/{elastic_response.jest.ts => elastic_response.test.ts} (100%)
 rename public/app/plugins/datasource/elasticsearch/specs/{index_pattern.jest.ts => index_pattern.test.ts} (100%)
 rename public/app/plugins/datasource/elasticsearch/specs/{query_builder.jest.ts => query_builder.test.ts} (100%)
 rename public/app/plugins/datasource/elasticsearch/specs/{query_def.jest.ts => query_def.test.ts} (100%)
 rename public/app/plugins/datasource/graphite/specs/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/graphite/specs/{gfunc.jest.ts => gfunc.test.ts} (100%)
 rename public/app/plugins/datasource/graphite/specs/{graphite_query.jest.ts => graphite_query.test.ts} (100%)
 rename public/app/plugins/datasource/graphite/specs/{lexer.jest.ts => lexer.test.ts} (100%)
 rename public/app/plugins/datasource/graphite/specs/{parser.jest.ts => parser.test.ts} (100%)
 rename public/app/plugins/datasource/graphite/specs/{query_ctrl.jest.ts => query_ctrl.test.ts} (100%)
 rename public/app/plugins/datasource/influxdb/specs/{influx_query.jest.ts => influx_query.test.ts} (100%)
 rename public/app/plugins/datasource/influxdb/specs/{influx_series.jest.ts => influx_series.test.ts} (100%)
 rename public/app/plugins/datasource/influxdb/specs/{query_builder.jest.ts => query_builder.test.ts} (100%)
 rename public/app/plugins/datasource/influxdb/specs/{query_ctrl.jest.ts => query_ctrl.test.ts} (100%)
 rename public/app/plugins/datasource/influxdb/specs/{query_part.jest.ts => query_part.test.ts} (100%)
 rename public/app/plugins/datasource/influxdb/specs/{response_parser.jest.ts => response_parser.test.ts} (100%)
 rename public/app/plugins/datasource/logging/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/logging/{result_transformer.jest.ts => result_transformer.test.ts} (100%)
 rename public/app/plugins/datasource/mssql/specs/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/mysql/specs/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/opentsdb/specs/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/opentsdb/specs/{query_ctrl.jest.ts => query_ctrl.test.ts} (100%)
 rename public/app/plugins/datasource/postgres/specs/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/prometheus/specs/{completer.jest.ts => completer.test.ts} (100%)
 rename public/app/plugins/datasource/prometheus/specs/{datasource.jest.ts => datasource.test.ts} (100%)
 rename public/app/plugins/datasource/prometheus/specs/{metric_find_query.jest.ts => metric_find_query.test.ts} (100%)
 rename public/app/plugins/datasource/prometheus/specs/{result_transformer.jest.ts => result_transformer.test.ts} (100%)
 rename public/app/plugins/panel/graph/specs/{align_yaxes.jest.ts => align_yaxes.test.ts} (100%)
 rename public/app/plugins/panel/graph/specs/{data_processor.jest.ts => data_processor.test.ts} (100%)
 rename public/app/plugins/panel/graph/specs/{graph.jest.ts => graph.test.ts} (100%)
 rename public/app/plugins/panel/graph/specs/{graph_ctrl.jest.ts => graph_ctrl.test.ts} (100%)
 rename public/app/plugins/panel/graph/specs/{graph_tooltip.jest.ts => graph_tooltip.test.ts} (100%)
 rename public/app/plugins/panel/graph/specs/{histogram.jest.ts => histogram.test.ts} (100%)
 rename public/app/plugins/panel/graph/specs/{series_override_ctrl.jest.ts => series_override_ctrl.test.ts} (100%)
 rename public/app/plugins/panel/graph/specs/{threshold_manager.jest.ts => threshold_manager.test.ts} (100%)
 rename public/app/plugins/panel/heatmap/specs/{heatmap_ctrl.jest.ts => heatmap_ctrl.test.ts} (100%)
 rename public/app/plugins/panel/heatmap/specs/{heatmap_data_converter.jest.ts => heatmap_data_converter.test.ts} (100%)
 rename public/app/plugins/panel/singlestat/specs/{singlestat.jest.ts => singlestat.test.ts} (100%)
 rename public/app/plugins/panel/singlestat/specs/{singlestat_panel.jest.ts => singlestat_panel.test.ts} (100%)
 rename public/app/plugins/panel/table/specs/{renderer.jest.ts => renderer.test.ts} (100%)
 rename public/app/plugins/panel/table/specs/{transformers.jest.ts => transformers.test.ts} (100%)
 rename public/app/stores/AlertListStore/{AlertListStore.jest.ts => AlertListStore.test.ts} (100%)
 rename public/app/stores/NavStore/{NavStore.jest.ts => NavStore.test.ts} (100%)
 rename public/app/stores/PermissionsStore/{PermissionsStore.jest.ts => PermissionsStore.test.ts} (100%)
 rename public/app/stores/ViewStore/{ViewStore.jest.ts => ViewStore.test.ts} (100%)
 rename public/test/core/utils/{version_jest.ts => version_test.ts} (100%)

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index f0f4e19bfc3..14c6c07ab16 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -7,7 +7,7 @@ grunt && grunt watch
 
 ### Rerun tests on source change
 ```
-npm jest
+npm run jest
 ```
 or
 ```
diff --git a/jest.config.js b/jest.config.js
index 606465c9840..a5cd3416f75 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -13,7 +13,7 @@ module.exports = {
   "roots": [
     "<rootDir>/public"
   ],
-  "testRegex": "(\\.|/)(jest)\\.(jsx?|tsx?)$",
+  "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$",
   "moduleFileExtensions": [
     "ts",
     "tsx",
diff --git a/karma.conf.js b/karma.conf.js
deleted file mode 100644
index 352e8e4e027..00000000000
--- a/karma.conf.js
+++ /dev/null
@@ -1,40 +0,0 @@
-var webpack = require('webpack');
-var path = require('path');
-var webpackTestConfig = require('./scripts/webpack/webpack.test.js');
-
-module.exports = function(config) {
-
-  'use strict';
-
-  config.set({
-    frameworks: ['mocha', 'expect', 'sinon'],
-
-    // list of files / patterns to load in the browser
-    files: [
-      { pattern: 'public/test/index.ts', watched: false }
-    ],
-
-    preprocessors: {
-      'public/test/index.ts': ['webpack', 'sourcemap'],
-    },
-
-    webpack: webpackTestConfig,
-    webpackMiddleware: {
-      stats: 'minimal',
-    },
-
-    // list of files to exclude
-    exclude: [],
-    reporters: ['dots'],
-    port: 9876,
-    colors: true,
-    logLevel: config.LOG_INFO,
-    autoWatch: true,
-    browsers: ['PhantomJS'],
-    captureTimeout: 20000,
-    singleRun: true,
-    // autoWatchBatchDelay: 1000,
-    // browserNoActivityTimeout: 60000,
-  });
-
-};
diff --git a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx
similarity index 100%
rename from public/app/containers/AlertRuleList/AlertRuleList.jest.tsx
rename to public/app/containers/AlertRuleList/AlertRuleList.test.tsx
diff --git a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap
similarity index 100%
rename from public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap
rename to public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap
diff --git a/public/app/containers/Explore/PromQueryField.jest.tsx b/public/app/containers/Explore/PromQueryField.test.tsx
similarity index 100%
rename from public/app/containers/Explore/PromQueryField.jest.tsx
rename to public/app/containers/Explore/PromQueryField.test.tsx
diff --git a/public/app/containers/Explore/TimePicker.jest.tsx b/public/app/containers/Explore/TimePicker.test.tsx
similarity index 100%
rename from public/app/containers/Explore/TimePicker.jest.tsx
rename to public/app/containers/Explore/TimePicker.test.tsx
diff --git a/public/app/containers/Explore/slate-plugins/braces.jest.ts b/public/app/containers/Explore/slate-plugins/braces.test.ts
similarity index 100%
rename from public/app/containers/Explore/slate-plugins/braces.jest.ts
rename to public/app/containers/Explore/slate-plugins/braces.test.ts
diff --git a/public/app/containers/Explore/slate-plugins/clear.jest.ts b/public/app/containers/Explore/slate-plugins/clear.test.ts
similarity index 100%
rename from public/app/containers/Explore/slate-plugins/clear.jest.ts
rename to public/app/containers/Explore/slate-plugins/clear.test.ts
diff --git a/public/app/containers/Explore/utils/prometheus.jest.ts b/public/app/containers/Explore/utils/prometheus.test.ts
similarity index 100%
rename from public/app/containers/Explore/utils/prometheus.jest.ts
rename to public/app/containers/Explore/utils/prometheus.test.ts
diff --git a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx b/public/app/containers/ManageDashboards/FolderSettings.test.tsx
similarity index 100%
rename from public/app/containers/ManageDashboards/FolderSettings.jest.tsx
rename to public/app/containers/ManageDashboards/FolderSettings.test.tsx
diff --git a/public/app/containers/ServerStats/ServerStats.jest.tsx b/public/app/containers/ServerStats/ServerStats.test.tsx
similarity index 100%
rename from public/app/containers/ServerStats/ServerStats.jest.tsx
rename to public/app/containers/ServerStats/ServerStats.test.tsx
diff --git a/public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap b/public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap
similarity index 100%
rename from public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap
rename to public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap
diff --git a/public/app/core/components/DeleteButton/DeleteButton.jest.tsx b/public/app/core/components/DeleteButton/DeleteButton.test.tsx
similarity index 100%
rename from public/app/core/components/DeleteButton/DeleteButton.jest.tsx
rename to public/app/core/components/DeleteButton/DeleteButton.test.tsx
diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx
similarity index 100%
rename from public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx
rename to public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx
diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap
similarity index 100%
rename from public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap
rename to public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap
diff --git a/public/app/core/components/PageHeader/PageHeader.jest.tsx b/public/app/core/components/PageHeader/PageHeader.test.tsx
similarity index 100%
rename from public/app/core/components/PageHeader/PageHeader.jest.tsx
rename to public/app/core/components/PageHeader/PageHeader.test.tsx
diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.test.tsx
similarity index 100%
rename from public/app/core/components/Permissions/AddPermissions.jest.tsx
rename to public/app/core/components/Permissions/AddPermissions.test.tsx
diff --git a/public/app/core/components/Picker/PickerOption.jest.tsx b/public/app/core/components/Picker/PickerOption.test.tsx
similarity index 100%
rename from public/app/core/components/Picker/PickerOption.jest.tsx
rename to public/app/core/components/Picker/PickerOption.test.tsx
diff --git a/public/app/core/components/Picker/TeamPicker.jest.tsx b/public/app/core/components/Picker/TeamPicker.test.tsx
similarity index 100%
rename from public/app/core/components/Picker/TeamPicker.jest.tsx
rename to public/app/core/components/Picker/TeamPicker.test.tsx
diff --git a/public/app/core/components/Picker/UserPicker.jest.tsx b/public/app/core/components/Picker/UserPicker.test.tsx
similarity index 100%
rename from public/app/core/components/Picker/UserPicker.jest.tsx
rename to public/app/core/components/Picker/UserPicker.test.tsx
diff --git a/public/app/core/components/Picker/__snapshots__/PickerOption.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap
similarity index 100%
rename from public/app/core/components/Picker/__snapshots__/PickerOption.jest.tsx.snap
rename to public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap
diff --git a/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap
similarity index 100%
rename from public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap
rename to public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap
diff --git a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap
similarity index 100%
rename from public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap
rename to public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap
diff --git a/public/app/core/components/Tooltip/Popover.jest.tsx b/public/app/core/components/Tooltip/Popover.test.tsx
similarity index 100%
rename from public/app/core/components/Tooltip/Popover.jest.tsx
rename to public/app/core/components/Tooltip/Popover.test.tsx
diff --git a/public/app/core/components/Tooltip/Tooltip.jest.tsx b/public/app/core/components/Tooltip/Tooltip.test.tsx
similarity index 100%
rename from public/app/core/components/Tooltip/Tooltip.jest.tsx
rename to public/app/core/components/Tooltip/Tooltip.test.tsx
diff --git a/public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap
similarity index 100%
rename from public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap
rename to public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap
diff --git a/public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap
similarity index 100%
rename from public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap
rename to public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap
diff --git a/public/app/core/specs/ColorPalette.jest.tsx b/public/app/core/specs/ColorPalette.test.tsx
similarity index 100%
rename from public/app/core/specs/ColorPalette.jest.tsx
rename to public/app/core/specs/ColorPalette.test.tsx
diff --git a/public/app/core/specs/PasswordStrength.jest.tsx b/public/app/core/specs/PasswordStrength.test.tsx
similarity index 100%
rename from public/app/core/specs/PasswordStrength.jest.tsx
rename to public/app/core/specs/PasswordStrength.test.tsx
diff --git a/public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap b/public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap
similarity index 100%
rename from public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap
rename to public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap
diff --git a/public/app/core/specs/backend_srv.jest.ts b/public/app/core/specs/backend_srv.test.ts
similarity index 100%
rename from public/app/core/specs/backend_srv.jest.ts
rename to public/app/core/specs/backend_srv.test.ts
diff --git a/public/app/core/specs/datemath.jest.ts b/public/app/core/specs/datemath.test.ts
similarity index 100%
rename from public/app/core/specs/datemath.jest.ts
rename to public/app/core/specs/datemath.test.ts
diff --git a/public/app/core/specs/emitter.jest.ts b/public/app/core/specs/emitter.test.ts
similarity index 100%
rename from public/app/core/specs/emitter.jest.ts
rename to public/app/core/specs/emitter.test.ts
diff --git a/public/app/core/specs/file_export.jest.ts b/public/app/core/specs/file_export.test.ts
similarity index 100%
rename from public/app/core/specs/file_export.jest.ts
rename to public/app/core/specs/file_export.test.ts
diff --git a/public/app/core/specs/flatten.jest.ts b/public/app/core/specs/flatten.test.ts
similarity index 100%
rename from public/app/core/specs/flatten.jest.ts
rename to public/app/core/specs/flatten.test.ts
diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.test.ts
similarity index 100%
rename from public/app/core/specs/kbn.jest.ts
rename to public/app/core/specs/kbn.test.ts
diff --git a/public/app/core/specs/location_util.jest.ts b/public/app/core/specs/location_util.test.ts
similarity index 100%
rename from public/app/core/specs/location_util.jest.ts
rename to public/app/core/specs/location_util.test.ts
diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.test.ts
similarity index 100%
rename from public/app/core/specs/manage_dashboards.jest.ts
rename to public/app/core/specs/manage_dashboards.test.ts
diff --git a/public/app/core/specs/org_switcher.jest.ts b/public/app/core/specs/org_switcher.test.ts
similarity index 100%
rename from public/app/core/specs/org_switcher.jest.ts
rename to public/app/core/specs/org_switcher.test.ts
diff --git a/public/app/core/specs/rangeutil.jest.ts b/public/app/core/specs/rangeutil.test.ts
similarity index 100%
rename from public/app/core/specs/rangeutil.jest.ts
rename to public/app/core/specs/rangeutil.test.ts
diff --git a/public/app/core/specs/search.jest.ts b/public/app/core/specs/search.test.ts
similarity index 100%
rename from public/app/core/specs/search.jest.ts
rename to public/app/core/specs/search.test.ts
diff --git a/public/app/core/specs/search_results.jest.ts b/public/app/core/specs/search_results.test.ts
similarity index 100%
rename from public/app/core/specs/search_results.jest.ts
rename to public/app/core/specs/search_results.test.ts
diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.test.ts
similarity index 100%
rename from public/app/core/specs/search_srv.jest.ts
rename to public/app/core/specs/search_srv.test.ts
diff --git a/public/app/core/specs/store.jest.ts b/public/app/core/specs/store.test.ts
similarity index 100%
rename from public/app/core/specs/store.jest.ts
rename to public/app/core/specs/store.test.ts
diff --git a/public/app/core/specs/table_model.jest.ts b/public/app/core/specs/table_model.test.ts
similarity index 100%
rename from public/app/core/specs/table_model.jest.ts
rename to public/app/core/specs/table_model.test.ts
diff --git a/public/app/core/specs/ticks.jest.ts b/public/app/core/specs/ticks.test.ts
similarity index 100%
rename from public/app/core/specs/ticks.jest.ts
rename to public/app/core/specs/ticks.test.ts
diff --git a/public/app/core/specs/time_series.jest.ts b/public/app/core/specs/time_series.test.ts
similarity index 100%
rename from public/app/core/specs/time_series.jest.ts
rename to public/app/core/specs/time_series.test.ts
diff --git a/public/app/core/specs/value_select_dropdown.jest.ts b/public/app/core/specs/value_select_dropdown.test.ts
similarity index 100%
rename from public/app/core/specs/value_select_dropdown.jest.ts
rename to public/app/core/specs/value_select_dropdown.test.ts
diff --git a/public/app/features/alerting/specs/threshold_mapper.jest.ts b/public/app/features/alerting/specs/threshold_mapper.test.ts
similarity index 100%
rename from public/app/features/alerting/specs/threshold_mapper.jest.ts
rename to public/app/features/alerting/specs/threshold_mapper.test.ts
diff --git a/public/app/features/annotations/specs/annotations_srv.jest.ts b/public/app/features/annotations/specs/annotations_srv.test.ts
similarity index 100%
rename from public/app/features/annotations/specs/annotations_srv.jest.ts
rename to public/app/features/annotations/specs/annotations_srv.test.ts
diff --git a/public/app/features/annotations/specs/annotations_srv_specs.jest.ts b/public/app/features/annotations/specs/annotations_srv_specs.test.ts
similarity index 100%
rename from public/app/features/annotations/specs/annotations_srv_specs.jest.ts
rename to public/app/features/annotations/specs/annotations_srv_specs.test.ts
diff --git a/public/app/features/dashboard/specs/AddPanelPanel.jest.tsx b/public/app/features/dashboard/specs/AddPanelPanel.test.tsx
similarity index 100%
rename from public/app/features/dashboard/specs/AddPanelPanel.jest.tsx
rename to public/app/features/dashboard/specs/AddPanelPanel.test.tsx
diff --git a/public/app/features/dashboard/specs/DashboardRow.jest.tsx b/public/app/features/dashboard/specs/DashboardRow.test.tsx
similarity index 100%
rename from public/app/features/dashboard/specs/DashboardRow.jest.tsx
rename to public/app/features/dashboard/specs/DashboardRow.test.tsx
diff --git a/public/app/features/dashboard/specs/change_tracker.jest.ts b/public/app/features/dashboard/specs/change_tracker.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/change_tracker.jest.ts
rename to public/app/features/dashboard/specs/change_tracker.test.ts
diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts
rename to public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts
diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/dashboard_migration.jest.ts
rename to public/app/features/dashboard/specs/dashboard_migration.test.ts
diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/dashboard_model.jest.ts
rename to public/app/features/dashboard/specs/dashboard_model.test.ts
diff --git a/public/app/features/dashboard/specs/exporter.jest.ts b/public/app/features/dashboard/specs/exporter.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/exporter.jest.ts
rename to public/app/features/dashboard/specs/exporter.test.ts
diff --git a/public/app/features/dashboard/specs/history_ctrl.jest.ts b/public/app/features/dashboard/specs/history_ctrl.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/history_ctrl.jest.ts
rename to public/app/features/dashboard/specs/history_ctrl.test.ts
diff --git a/public/app/features/dashboard/specs/history_srv.jest.ts b/public/app/features/dashboard/specs/history_srv.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/history_srv.jest.ts
rename to public/app/features/dashboard/specs/history_srv.test.ts
diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/repeat.jest.ts
rename to public/app/features/dashboard/specs/repeat.test.ts
diff --git a/public/app/features/dashboard/specs/save_as_modal.jest.ts b/public/app/features/dashboard/specs/save_as_modal.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/save_as_modal.jest.ts
rename to public/app/features/dashboard/specs/save_as_modal.test.ts
diff --git a/public/app/features/dashboard/specs/save_modal.jest.ts b/public/app/features/dashboard/specs/save_modal.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/save_modal.jest.ts
rename to public/app/features/dashboard/specs/save_modal.test.ts
diff --git a/public/app/features/dashboard/specs/save_provisioned_modal.jest.ts b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/save_provisioned_modal.jest.ts
rename to public/app/features/dashboard/specs/save_provisioned_modal.test.ts
diff --git a/public/app/features/dashboard/specs/time_srv.jest.ts b/public/app/features/dashboard/specs/time_srv.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/time_srv.jest.ts
rename to public/app/features/dashboard/specs/time_srv.test.ts
diff --git a/public/app/features/dashboard/specs/viewstate_srv.jest.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/viewstate_srv.jest.ts
rename to public/app/features/dashboard/specs/viewstate_srv.test.ts
diff --git a/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts
similarity index 100%
rename from public/app/features/panel/specs/metrics_panel_ctrl.jest.ts
rename to public/app/features/panel/specs/metrics_panel_ctrl.test.ts
diff --git a/public/app/features/panellinks/specs/link_srv.jest.ts b/public/app/features/panellinks/specs/link_srv.test.ts
similarity index 100%
rename from public/app/features/panellinks/specs/link_srv.jest.ts
rename to public/app/features/panellinks/specs/link_srv.test.ts
diff --git a/public/app/features/playlist/specs/playlist_edit_ctrl.jest.ts b/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts
similarity index 100%
rename from public/app/features/playlist/specs/playlist_edit_ctrl.jest.ts
rename to public/app/features/playlist/specs/playlist_edit_ctrl.test.ts
diff --git a/public/app/features/plugins/specs/datasource_srv.jest.ts b/public/app/features/plugins/specs/datasource_srv.test.ts
similarity index 100%
rename from public/app/features/plugins/specs/datasource_srv.jest.ts
rename to public/app/features/plugins/specs/datasource_srv.test.ts
diff --git a/public/app/features/templating/specs/adhoc_variable.jest.ts b/public/app/features/templating/specs/adhoc_variable.test.ts
similarity index 100%
rename from public/app/features/templating/specs/adhoc_variable.jest.ts
rename to public/app/features/templating/specs/adhoc_variable.test.ts
diff --git a/public/app/features/templating/specs/editor_ctrl.jest.ts b/public/app/features/templating/specs/editor_ctrl.test.ts
similarity index 100%
rename from public/app/features/templating/specs/editor_ctrl.jest.ts
rename to public/app/features/templating/specs/editor_ctrl.test.ts
diff --git a/public/app/features/templating/specs/query_variable.jest.ts b/public/app/features/templating/specs/query_variable.test.ts
similarity index 100%
rename from public/app/features/templating/specs/query_variable.jest.ts
rename to public/app/features/templating/specs/query_variable.test.ts
diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.test.ts
similarity index 100%
rename from public/app/features/templating/specs/template_srv.jest.ts
rename to public/app/features/templating/specs/template_srv.test.ts
diff --git a/public/app/features/templating/specs/variable.jest.ts b/public/app/features/templating/specs/variable.test.ts
similarity index 100%
rename from public/app/features/templating/specs/variable.jest.ts
rename to public/app/features/templating/specs/variable.test.ts
diff --git a/public/app/features/templating/specs/variable_srv.jest.ts b/public/app/features/templating/specs/variable_srv.test.ts
similarity index 100%
rename from public/app/features/templating/specs/variable_srv.jest.ts
rename to public/app/features/templating/specs/variable_srv.test.ts
diff --git a/public/app/features/templating/specs/variable_srv_init.jest.ts b/public/app/features/templating/specs/variable_srv_init.test.ts
similarity index 100%
rename from public/app/features/templating/specs/variable_srv_init.jest.ts
rename to public/app/features/templating/specs/variable_srv_init.test.ts
diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts
rename to public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts
diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/elasticsearch/specs/datasource.jest.ts
rename to public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts
diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts
similarity index 100%
rename from public/app/plugins/datasource/elasticsearch/specs/elastic_response.jest.ts
rename to public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts
diff --git a/public/app/plugins/datasource/elasticsearch/specs/index_pattern.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts
similarity index 100%
rename from public/app/plugins/datasource/elasticsearch/specs/index_pattern.jest.ts
rename to public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts
diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts
similarity index 100%
rename from public/app/plugins/datasource/elasticsearch/specs/query_builder.jest.ts
rename to public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts
diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_def.jest.ts b/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts
similarity index 100%
rename from public/app/plugins/datasource/elasticsearch/specs/query_def.jest.ts
rename to public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts
diff --git a/public/app/plugins/datasource/graphite/specs/datasource.jest.ts b/public/app/plugins/datasource/graphite/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/graphite/specs/datasource.jest.ts
rename to public/app/plugins/datasource/graphite/specs/datasource.test.ts
diff --git a/public/app/plugins/datasource/graphite/specs/gfunc.jest.ts b/public/app/plugins/datasource/graphite/specs/gfunc.test.ts
similarity index 100%
rename from public/app/plugins/datasource/graphite/specs/gfunc.jest.ts
rename to public/app/plugins/datasource/graphite/specs/gfunc.test.ts
diff --git a/public/app/plugins/datasource/graphite/specs/graphite_query.jest.ts b/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts
similarity index 100%
rename from public/app/plugins/datasource/graphite/specs/graphite_query.jest.ts
rename to public/app/plugins/datasource/graphite/specs/graphite_query.test.ts
diff --git a/public/app/plugins/datasource/graphite/specs/lexer.jest.ts b/public/app/plugins/datasource/graphite/specs/lexer.test.ts
similarity index 100%
rename from public/app/plugins/datasource/graphite/specs/lexer.jest.ts
rename to public/app/plugins/datasource/graphite/specs/lexer.test.ts
diff --git a/public/app/plugins/datasource/graphite/specs/parser.jest.ts b/public/app/plugins/datasource/graphite/specs/parser.test.ts
similarity index 100%
rename from public/app/plugins/datasource/graphite/specs/parser.jest.ts
rename to public/app/plugins/datasource/graphite/specs/parser.test.ts
diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts
similarity index 100%
rename from public/app/plugins/datasource/graphite/specs/query_ctrl.jest.ts
rename to public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts
diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query.jest.ts b/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts
similarity index 100%
rename from public/app/plugins/datasource/influxdb/specs/influx_query.jest.ts
rename to public/app/plugins/datasource/influxdb/specs/influx_query.test.ts
diff --git a/public/app/plugins/datasource/influxdb/specs/influx_series.jest.ts b/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts
similarity index 100%
rename from public/app/plugins/datasource/influxdb/specs/influx_series.jest.ts
rename to public/app/plugins/datasource/influxdb/specs/influx_series.test.ts
diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts
similarity index 100%
rename from public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts
rename to public/app/plugins/datasource/influxdb/specs/query_builder.test.ts
diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts
similarity index 100%
rename from public/app/plugins/datasource/influxdb/specs/query_ctrl.jest.ts
rename to public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts
diff --git a/public/app/plugins/datasource/influxdb/specs/query_part.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_part.test.ts
similarity index 100%
rename from public/app/plugins/datasource/influxdb/specs/query_part.jest.ts
rename to public/app/plugins/datasource/influxdb/specs/query_part.test.ts
diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts b/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts
similarity index 100%
rename from public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts
rename to public/app/plugins/datasource/influxdb/specs/response_parser.test.ts
diff --git a/public/app/plugins/datasource/logging/datasource.jest.ts b/public/app/plugins/datasource/logging/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/logging/datasource.jest.ts
rename to public/app/plugins/datasource/logging/datasource.test.ts
diff --git a/public/app/plugins/datasource/logging/result_transformer.jest.ts b/public/app/plugins/datasource/logging/result_transformer.test.ts
similarity index 100%
rename from public/app/plugins/datasource/logging/result_transformer.jest.ts
rename to public/app/plugins/datasource/logging/result_transformer.test.ts
diff --git a/public/app/plugins/datasource/mssql/specs/datasource.jest.ts b/public/app/plugins/datasource/mssql/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/mssql/specs/datasource.jest.ts
rename to public/app/plugins/datasource/mssql/specs/datasource.test.ts
diff --git a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/mysql/specs/datasource.jest.ts
rename to public/app/plugins/datasource/mysql/specs/datasource.test.ts
diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/opentsdb/specs/datasource.jest.ts
rename to public/app/plugins/datasource/opentsdb/specs/datasource.test.ts
diff --git a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts
similarity index 100%
rename from public/app/plugins/datasource/opentsdb/specs/query_ctrl.jest.ts
rename to public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts
diff --git a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts b/public/app/plugins/datasource/postgres/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/postgres/specs/datasource.jest.ts
rename to public/app/plugins/datasource/postgres/specs/datasource.test.ts
diff --git a/public/app/plugins/datasource/prometheus/specs/completer.jest.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts
similarity index 100%
rename from public/app/plugins/datasource/prometheus/specs/completer.jest.ts
rename to public/app/plugins/datasource/prometheus/specs/completer.test.ts
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
rename to public/app/plugins/datasource/prometheus/specs/datasource.test.ts
diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts
similarity index 100%
rename from public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts
rename to public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts
diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts
similarity index 100%
rename from public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts
rename to public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts
diff --git a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts b/public/app/plugins/panel/graph/specs/align_yaxes.test.ts
similarity index 100%
rename from public/app/plugins/panel/graph/specs/align_yaxes.jest.ts
rename to public/app/plugins/panel/graph/specs/align_yaxes.test.ts
diff --git a/public/app/plugins/panel/graph/specs/data_processor.jest.ts b/public/app/plugins/panel/graph/specs/data_processor.test.ts
similarity index 100%
rename from public/app/plugins/panel/graph/specs/data_processor.jest.ts
rename to public/app/plugins/panel/graph/specs/data_processor.test.ts
diff --git a/public/app/plugins/panel/graph/specs/graph.jest.ts b/public/app/plugins/panel/graph/specs/graph.test.ts
similarity index 100%
rename from public/app/plugins/panel/graph/specs/graph.jest.ts
rename to public/app/plugins/panel/graph/specs/graph.test.ts
diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts
similarity index 100%
rename from public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
rename to public/app/plugins/panel/graph/specs/graph_ctrl.test.ts
diff --git a/public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts b/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts
similarity index 100%
rename from public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts
rename to public/app/plugins/panel/graph/specs/graph_tooltip.test.ts
diff --git a/public/app/plugins/panel/graph/specs/histogram.jest.ts b/public/app/plugins/panel/graph/specs/histogram.test.ts
similarity index 100%
rename from public/app/plugins/panel/graph/specs/histogram.jest.ts
rename to public/app/plugins/panel/graph/specs/histogram.test.ts
diff --git a/public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts
similarity index 100%
rename from public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts
rename to public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts
diff --git a/public/app/plugins/panel/graph/specs/threshold_manager.jest.ts b/public/app/plugins/panel/graph/specs/threshold_manager.test.ts
similarity index 100%
rename from public/app/plugins/panel/graph/specs/threshold_manager.jest.ts
rename to public/app/plugins/panel/graph/specs/threshold_manager.test.ts
diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts
similarity index 100%
rename from public/app/plugins/panel/heatmap/specs/heatmap_ctrl.jest.ts
rename to public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts
diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts
similarity index 100%
rename from public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts
rename to public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts
diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts
similarity index 100%
rename from public/app/plugins/panel/singlestat/specs/singlestat.jest.ts
rename to public/app/plugins/panel/singlestat/specs/singlestat.test.ts
diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_panel.jest.ts b/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts
similarity index 100%
rename from public/app/plugins/panel/singlestat/specs/singlestat_panel.jest.ts
rename to public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts
diff --git a/public/app/plugins/panel/table/specs/renderer.jest.ts b/public/app/plugins/panel/table/specs/renderer.test.ts
similarity index 100%
rename from public/app/plugins/panel/table/specs/renderer.jest.ts
rename to public/app/plugins/panel/table/specs/renderer.test.ts
diff --git a/public/app/plugins/panel/table/specs/transformers.jest.ts b/public/app/plugins/panel/table/specs/transformers.test.ts
similarity index 100%
rename from public/app/plugins/panel/table/specs/transformers.jest.ts
rename to public/app/plugins/panel/table/specs/transformers.test.ts
diff --git a/public/app/stores/AlertListStore/AlertListStore.jest.ts b/public/app/stores/AlertListStore/AlertListStore.test.ts
similarity index 100%
rename from public/app/stores/AlertListStore/AlertListStore.jest.ts
rename to public/app/stores/AlertListStore/AlertListStore.test.ts
diff --git a/public/app/stores/NavStore/NavStore.jest.ts b/public/app/stores/NavStore/NavStore.test.ts
similarity index 100%
rename from public/app/stores/NavStore/NavStore.jest.ts
rename to public/app/stores/NavStore/NavStore.test.ts
diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.test.ts
similarity index 100%
rename from public/app/stores/PermissionsStore/PermissionsStore.jest.ts
rename to public/app/stores/PermissionsStore/PermissionsStore.test.ts
diff --git a/public/app/stores/ViewStore/ViewStore.jest.ts b/public/app/stores/ViewStore/ViewStore.test.ts
similarity index 100%
rename from public/app/stores/ViewStore/ViewStore.jest.ts
rename to public/app/stores/ViewStore/ViewStore.test.ts
diff --git a/public/test/core/utils/version_jest.ts b/public/test/core/utils/version_test.ts
similarity index 100%
rename from public/test/core/utils/version_jest.ts
rename to public/test/core/utils/version_test.ts

From 86a27895415fa28b8850852fdbfb4ab3ff793dca Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <tobias.skarhed@gmail.com>
Date: Tue, 14 Aug 2018 11:23:55 +0200
Subject: [PATCH 331/380] Remove dependencies

---
 yarn.lock | 549 ++++--------------------------------------------------
 1 file changed, 33 insertions(+), 516 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 89e74828351..c4bd6704839 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -422,13 +422,6 @@ abbrev@1, abbrev@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
 
-accepts@1.3.3:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
-  dependencies:
-    mime-types "~2.1.11"
-    negotiator "0.6.1"
-
 accepts@~1.3.4, accepts@~1.3.5:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
@@ -480,10 +473,6 @@ add-dom-event-listener@1.x:
   dependencies:
     object-assign "4.x"
 
-after@0.8.2:
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
-
 agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce"
@@ -769,10 +758,6 @@ array-reduce@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
 
-array-slice@^0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
-
 array-tree-filter@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-1.0.1.tgz#0a8ad1eefd38ce88858632f9cc0423d7634e4d5d"
@@ -795,10 +780,6 @@ array-unique@^0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
 
-arraybuffer.slice@0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca"
-
 arrify@^1.0.0, arrify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@@ -1520,7 +1501,7 @@ babel-register@^6.26.0, babel-register@^6.9.0:
     mkdirp "^0.5.1"
     source-map-support "^0.4.15"
 
-babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2:
+babel-runtime@6.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   dependencies:
@@ -1568,10 +1549,6 @@ babylon@^7.0.0-beta.47:
   version "7.0.0-beta.47"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.47.tgz#6d1fa44f0abec41ab7c780481e62fd9aafbdea80"
 
-backo2@1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
-
 balanced-match@^0.4.2:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -1584,18 +1561,10 @@ baron@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/baron/-/baron-3.0.3.tgz#0f0a08a567062882e130a0ecfd41a46d52103f4a"
 
-base64-arraybuffer@0.1.5:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
-
 base64-js@^1.0.2:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
 
-base64id@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
-
 base@^0.11.1:
   version "0.11.2"
   resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@@ -1622,12 +1591,6 @@ bcrypt-pbkdf@^1.0.0:
   dependencies:
     tweetnacl "^0.14.3"
 
-better-assert@~1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
-  dependencies:
-    callsite "1.0.0"
-
 bfj-node4@^5.2.0:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/bfj-node4/-/bfj-node4-5.3.1.tgz#e23d8b27057f1d0214fc561142ad9db998f26830"
@@ -1665,17 +1628,13 @@ bl@^1.0.0:
     readable-stream "^2.3.5"
     safe-buffer "^5.1.1"
 
-blob@0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
-
 block-stream@*:
   version "0.0.9"
   resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
   dependencies:
     inherits "~2.0.0"
 
-bluebird@^3.3.0, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@~3.5.1:
+bluebird@^3.5.0, bluebird@^3.5.1, bluebird@~3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
 
@@ -1698,21 +1657,6 @@ body-parser@1.18.2:
     raw-body "2.3.2"
     type-is "~1.6.15"
 
-body-parser@^1.16.1:
-  version "1.18.3"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
-  dependencies:
-    bytes "3.0.0"
-    content-type "~1.0.4"
-    debug "2.6.9"
-    depd "~1.1.2"
-    http-errors "~1.6.3"
-    iconv-lite "0.4.23"
-    on-finished "~2.3.0"
-    qs "6.5.2"
-    raw-body "2.3.3"
-    type-is "~1.6.16"
-
 bonjour@^3.5.0:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
@@ -1759,12 +1703,6 @@ brace@^0.10.0:
   dependencies:
     w3c-blob "0.0.1"
 
-braces@^0.1.2:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6"
-  dependencies:
-    expand-range "^0.1.0"
-
 braces@^1.8.2:
   version "1.8.5"
   resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
@@ -2021,10 +1959,6 @@ caller-path@^0.1.0:
   dependencies:
     callsites "^0.2.0"
 
-callsite@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
-
 callsites@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
@@ -2169,7 +2103,7 @@ cheerio@^1.0.0-rc.2:
     lodash "^4.15.0"
     parse5 "^3.0.1"
 
-chokidar@^1.4.1, chokidar@^1.6.0, chokidar@^1.7.0:
+chokidar@^1.6.0, chokidar@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
   dependencies:
@@ -2479,7 +2413,7 @@ colors@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
-colors@^1.1.0, colors@^1.1.2:
+colors@^1.1.2:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e"
 
@@ -2494,12 +2428,6 @@ columnify@~1.5.4:
     strip-ansi "^3.0.0"
     wcwidth "^1.0.0"
 
-combine-lists@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6"
-  dependencies:
-    lodash "^4.5.0"
-
 combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
@@ -2538,21 +2466,13 @@ compare-versions@^3.1.0:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.2.1.tgz#a49eb7689d4caaf0b6db5220173fd279614000f7"
 
-component-bind@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
-
 component-classes@^1.2.5:
   version "1.2.6"
   resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691"
   dependencies:
     component-indexof "0.0.3"
 
-component-emitter@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3"
-
-component-emitter@1.2.1, component-emitter@^1.2.1:
+component-emitter@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
 
@@ -2560,10 +2480,6 @@ component-indexof@0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24"
 
-component-inherit@0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
-
 compress-commons@^1.2.0:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.2.tgz#524a9f10903f3a813389b0225d27c48bb751890f"
@@ -2626,15 +2542,6 @@ connect-history-api-fallback@^1.3.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
 
-connect@^3.6.0:
-  version "3.6.6"
-  resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524"
-  dependencies:
-    debug "2.6.9"
-    finalhandler "1.1.0"
-    parseurl "~1.3.2"
-    utils-merge "1.0.1"
-
 console-browserify@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
@@ -2699,7 +2606,7 @@ core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
 
-core-js@^2.0.0, core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
+core-js@^2.0.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
   version "2.5.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
 
@@ -2959,10 +2866,6 @@ currently-unhandled@^0.4.1:
   dependencies:
     array-find-index "^1.0.1"
 
-custom-event@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
-
 cyclist@~0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@@ -3241,18 +3144,6 @@ dateformat@~1.0.12:
     get-stdin "^4.0.1"
     meow "^3.3.0"
 
-debug@2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
-  dependencies:
-    ms "0.7.1"
-
-debug@2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c"
-  dependencies:
-    ms "0.7.2"
-
 debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -3442,10 +3333,6 @@ dezalgo@^1.0.0, dezalgo@~1.0.3:
     asap "^2.0.0"
     wrappy "1"
 
-di@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
-
 diff-match-patch@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.1.tgz#d5f880213d82fbc124d2b95111fb3c033dbad7fa"
@@ -3523,15 +3410,6 @@ dom-helpers@^3.3.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
 
-dom-serialize@^2.2.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
-  dependencies:
-    custom-event "~1.0.0"
-    ent "~2.2.0"
-    extend "^3.0.0"
-    void-elements "^2.0.0"
-
 dom-serializer@0, dom-serializer@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@@ -3703,7 +3581,7 @@ empower@^1.2.3:
     core-js "^2.0.0"
     empower-core "^0.6.2"
 
-encodeurl@~1.0.1, encodeurl@~1.0.2:
+encodeurl@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
 
@@ -3719,45 +3597,6 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
   dependencies:
     once "^1.4.0"
 
-engine.io-client@1.8.3:
-  version "1.8.3"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab"
-  dependencies:
-    component-emitter "1.2.1"
-    component-inherit "0.0.3"
-    debug "2.3.3"
-    engine.io-parser "1.3.2"
-    has-cors "1.1.0"
-    indexof "0.0.1"
-    parsejson "0.0.3"
-    parseqs "0.0.5"
-    parseuri "0.0.5"
-    ws "1.1.2"
-    xmlhttprequest-ssl "1.5.3"
-    yeast "0.1.2"
-
-engine.io-parser@1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.2.tgz#937b079f0007d0893ec56d46cb220b8cb435220a"
-  dependencies:
-    after "0.8.2"
-    arraybuffer.slice "0.0.6"
-    base64-arraybuffer "0.1.5"
-    blob "0.0.4"
-    has-binary "0.1.7"
-    wtf-8 "1.0.0"
-
-engine.io@1.8.3:
-  version "1.8.3"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4"
-  dependencies:
-    accepts "1.3.3"
-    base64id "1.0.0"
-    cookie "0.3.1"
-    debug "2.3.3"
-    engine.io-parser "1.3.2"
-    ws "1.1.2"
-
 enhanced-resolve@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a"
@@ -3766,10 +3605,6 @@ enhanced-resolve@^4.0.0:
     memory-fs "^0.4.0"
     tapable "^1.0.0"
 
-ent@~2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
-
 entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
@@ -4138,14 +3973,6 @@ exit@^0.1.2, exit@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
 
-expand-braces@^0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea"
-  dependencies:
-    array-slice "^0.2.3"
-    array-unique "^0.2.1"
-    braces "^0.1.2"
-
 expand-brackets@^0.1.4:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
@@ -4164,13 +3991,6 @@ expand-brackets@^2.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
-expand-range@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044"
-  dependencies:
-    is-number "^0.1.1"
-    repeat-string "^0.2.2"
-
 expand-range@^1.8.1:
   version "1.8.2"
   resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
@@ -4187,10 +4007,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
   dependencies:
     homedir-polyfill "^1.0.1"
 
-expect.js@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.3.1.tgz#b0a59a0d2eff5437544ebf0ceaa6015841d09b5b"
-
 expect.js@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.2.0.tgz#1028533d2c1c363f74a6796ff57ec0520ded2be1"
@@ -4258,7 +4074,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
     assign-symbols "^1.0.0"
     is-extendable "^1.0.1"
 
-extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
+extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 
@@ -4455,18 +4271,6 @@ fill-range@^4.0.0:
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
-finalhandler@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5"
-  dependencies:
-    debug "2.6.9"
-    encodeurl "~1.0.1"
-    escape-html "~1.0.3"
-    on-finished "~2.3.0"
-    parseurl "~1.3.2"
-    statuses "~1.3.1"
-    unpipe "~1.0.0"
-
 finalhandler@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
@@ -4654,12 +4458,6 @@ front-matter@2.1.2:
   dependencies:
     js-yaml "^3.4.6"
 
-fs-access@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a"
-  dependencies:
-    null-check "^1.0.0"
-
 fs-constants@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -5152,12 +4950,6 @@ grunt-exec@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/grunt-exec/-/grunt-exec-1.0.1.tgz#e5d53a39c5f346901305edee5c87db0f2af999c4"
 
-grunt-karma@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/grunt-karma/-/grunt-karma-2.0.0.tgz#753583d115dfdc055fe57e58f96d6b3c7e612118"
-  dependencies:
-    lodash "^3.10.1"
-
 grunt-known-options@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.0.tgz#a4274eeb32fa765da5a7a3b1712617ce3b144149"
@@ -5311,20 +5103,10 @@ has-ansi@^2.0.0:
   dependencies:
     ansi-regex "^2.0.0"
 
-has-binary@0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c"
-  dependencies:
-    isarray "0.0.1"
-
 has-color@~0.1.0:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
 
-has-cors@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
-
 has-flag@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
@@ -5583,7 +5365,7 @@ http-errors@1.6.2:
     setprototypeof "1.0.3"
     statuses ">= 1.3.1 < 2"
 
-http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
+http-errors@~1.6.2:
   version "1.6.3"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
   dependencies:
@@ -5612,7 +5394,7 @@ http-proxy-middleware@~0.18.0:
     lodash "^4.17.5"
     micromatch "^3.1.9"
 
-http-proxy@^1.13.0, http-proxy@^1.16.2:
+http-proxy@^1.16.2:
   version "1.17.0"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
   dependencies:
@@ -5661,7 +5443,7 @@ husky@^0.14.3:
     normalize-path "^1.0.0"
     strip-indent "^2.0.0"
 
-iconv-lite@0.4, iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+iconv-lite@0.4, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
   version "0.4.23"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
   dependencies:
@@ -6057,10 +5839,6 @@ is-number-object@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
 
-is-number@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806"
-
 is-number@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@@ -6229,7 +6007,7 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
 
-isbinaryfile@^3.0.0, isbinaryfile@^3.0.2:
+isbinaryfile@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
 
@@ -6781,7 +6559,7 @@ json-stringify-safe@~5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
 
-json3@3.3.2, json3@^3.3.2:
+json3@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
 
@@ -6828,85 +6606,6 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
-karma-chrome-launcher@~2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf"
-  dependencies:
-    fs-access "^1.0.0"
-    which "^1.2.1"
-
-karma-expect@~1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/karma-expect/-/karma-expect-1.1.3.tgz#c6b0a56ff18903db11af4f098cc6e7cf198ce275"
-  dependencies:
-    expect.js "^0.3.1"
-
-karma-mocha@~1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf"
-  dependencies:
-    minimist "1.2.0"
-
-karma-phantomjs-launcher@1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz#d23ca34801bda9863ad318e3bb4bd4062b13acd2"
-  dependencies:
-    lodash "^4.0.1"
-    phantomjs-prebuilt "^2.1.7"
-
-karma-sinon@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/karma-sinon/-/karma-sinon-1.0.5.tgz#4e3443f2830fdecff624d3747163f1217daa2a9a"
-
-karma-sourcemap-loader@^0.3.7:
-  version "0.3.7"
-  resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8"
-  dependencies:
-    graceful-fs "^4.1.2"
-
-karma-webpack@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-3.0.0.tgz#bf009c5b73c667c11c015717e9e520f581317c44"
-  dependencies:
-    async "^2.0.0"
-    babel-runtime "^6.0.0"
-    loader-utils "^1.0.0"
-    lodash "^4.0.0"
-    source-map "^0.5.6"
-    webpack-dev-middleware "^2.0.6"
-
-karma@1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.0.tgz#6f7a1a406446fa2e187ec95398698f4cee476269"
-  dependencies:
-    bluebird "^3.3.0"
-    body-parser "^1.16.1"
-    chokidar "^1.4.1"
-    colors "^1.1.0"
-    combine-lists "^1.0.0"
-    connect "^3.6.0"
-    core-js "^2.2.0"
-    di "^0.0.1"
-    dom-serialize "^2.2.0"
-    expand-braces "^0.1.1"
-    glob "^7.1.1"
-    graceful-fs "^4.1.2"
-    http-proxy "^1.13.0"
-    isbinaryfile "^3.0.0"
-    lodash "^3.8.0"
-    log4js "^0.6.31"
-    mime "^1.3.4"
-    minimatch "^3.0.2"
-    optimist "^0.6.1"
-    qjobs "^1.1.4"
-    range-parser "^1.2.0"
-    rimraf "^2.6.0"
-    safe-buffer "^5.0.1"
-    socket.io "1.7.3"
-    source-map "^0.5.3"
-    tmp "0.0.31"
-    useragent "^2.1.12"
-
 kew@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"
@@ -7164,7 +6863,7 @@ loader-runner@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
 
-loader-utils@1.1.0, loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0:
+loader-utils@1.1.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
   dependencies:
@@ -7320,11 +7019,11 @@ lodash.without@~4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
 
-lodash@^3.10.1, lodash@^3.6.0, lodash@^3.8.0:
+lodash@^3.10.1, lodash@^3.6.0:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
-lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.5:
+lodash@^4.0.0, lodash@^4.1.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.5:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
@@ -7351,13 +7050,6 @@ log-update@^1.0.2:
     ansi-escapes "^1.0.0"
     cli-cursor "^1.0.2"
 
-log4js@^0.6.31:
-  version "0.6.38"
-  resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd"
-  dependencies:
-    readable-stream "~1.0.2"
-    semver "~4.3.3"
-
 loglevel@^1.4.1:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
@@ -7412,7 +7104,7 @@ lowercase-keys@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
 
-lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
+lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
   version "4.1.3"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
   dependencies:
@@ -7654,7 +7346,7 @@ mime-db@~1.33.0:
   version "1.33.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
 
-mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7:
+mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7:
   version "2.1.18"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
   dependencies:
@@ -7664,10 +7356,6 @@ mime@1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
 
-mime@^1.3.4:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
-
 mime@^2.1.0:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
@@ -7721,14 +7409,14 @@ minimist@1.1.x:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8"
 
-minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
-
 minimist@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de"
 
+minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
 minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
@@ -7867,14 +7555,6 @@ move-concurrently@^1.0.1:
     rimraf "^2.5.4"
     run-queue "^1.0.3"
 
-ms@0.7.1:
-  version "0.7.1"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
-
-ms@0.7.2:
-  version "0.7.2"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
-
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -8489,10 +8169,6 @@ nth-check@~1.0.1:
   dependencies:
     boolbase "~1.0.0"
 
-null-check@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
-
 num2fraction@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@@ -8509,18 +8185,10 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
 
-object-assign@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
-
 object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
 
-object-component@0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
-
 object-copy@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
@@ -8659,10 +8327,6 @@ optionator@^0.8.1:
     type-check "~0.3.2"
     wordwrap "~1.0.0"
 
-options@>=0.0.5:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
-
 ora@^0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
@@ -8710,7 +8374,7 @@ os-locale@^2.0.0:
     lcid "^1.0.0"
     mem "^1.1.0"
 
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
 
@@ -8919,24 +8583,6 @@ parse5@^3.0.1, parse5@^3.0.3:
   dependencies:
     "@types/node" "*"
 
-parsejson@0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.3.tgz#ab7e3759f209ece99437973f7d0f1f64ae0e64ab"
-  dependencies:
-    better-assert "~1.0.0"
-
-parseqs@0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
-  dependencies:
-    better-assert "~1.0.0"
-
-parseuri@0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
-  dependencies:
-    better-assert "~1.0.0"
-
 parseurl@~1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
@@ -9032,7 +8678,7 @@ performance-now@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
 
-phantomjs-prebuilt@^2.1.15, phantomjs-prebuilt@^2.1.7:
+phantomjs-prebuilt@^2.1.15:
   version "2.1.16"
   resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef"
   dependencies:
@@ -9697,10 +9343,6 @@ q@^1.1.2:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
 
-qjobs@^1.1.4:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
-
 qrcode-terminal@^0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819"
@@ -9709,14 +9351,14 @@ qs@6.5.1:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
 
-qs@6.5.2, qs@~6.5.1:
-  version "6.5.2"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
-
 qs@~6.3.0:
   version "6.3.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
 
+qs@~6.5.1:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+
 query-string@^4.1.0:
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
@@ -9793,7 +9435,7 @@ randomfill@^1.0.3:
     randombytes "^2.0.5"
     safe-buffer "^5.1.0"
 
-range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0:
+range-parser@^1.0.3, range-parser@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
 
@@ -9806,15 +9448,6 @@ raw-body@2.3.2:
     iconv-lite "0.4.19"
     unpipe "1.0.0"
 
-raw-body@2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
-  dependencies:
-    bytes "3.0.0"
-    http-errors "1.6.3"
-    iconv-lite "0.4.23"
-    unpipe "1.0.0"
-
 rc-align@^2.4.0:
   version "2.4.3"
   resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.3.tgz#b9b3c2a6d68adae71a8e1d041cd5e3b2a655f99a"
@@ -10101,7 +9734,7 @@ read@1, read@~1.0.1, read@~1.0.7:
     string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
-readable-stream@1.0, readable-stream@~1.0.2:
+readable-stream@1.0:
   version "1.0.34"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
   dependencies:
@@ -10311,10 +9944,6 @@ repeat-element@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
 
-repeat-string@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae"
-
 repeat-string@^1.5.2, repeat-string@^1.6.1:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
@@ -10526,7 +10155,7 @@ right-align@^0.1.1:
   dependencies:
     align-text "^0.1.1"
 
-rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -10727,10 +10356,6 @@ semver-diff@^2.0.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
 
-semver@~4.3.3:
-  version "4.3.6"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
-
 semver@~5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -11059,50 +10684,6 @@ sntp@1.x.x:
   dependencies:
     hoek "2.x.x"
 
-socket.io-adapter@0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b"
-  dependencies:
-    debug "2.3.3"
-    socket.io-parser "2.3.1"
-
-socket.io-client@1.7.3:
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377"
-  dependencies:
-    backo2 "1.0.2"
-    component-bind "1.0.0"
-    component-emitter "1.2.1"
-    debug "2.3.3"
-    engine.io-client "1.8.3"
-    has-binary "0.1.7"
-    indexof "0.0.1"
-    object-component "0.0.3"
-    parseuri "0.0.5"
-    socket.io-parser "2.3.1"
-    to-array "0.1.4"
-
-socket.io-parser@2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0"
-  dependencies:
-    component-emitter "1.1.2"
-    debug "2.2.0"
-    isarray "0.0.1"
-    json3 "3.3.2"
-
-socket.io@1.7.3:
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b"
-  dependencies:
-    debug "2.3.3"
-    engine.io "1.8.3"
-    has-binary "0.1.7"
-    object-assign "4.1.0"
-    socket.io-adapter "0.5.0"
-    socket.io-client "1.7.3"
-    socket.io-parser "2.3.1"
-
 sockjs-client@1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12"
@@ -11332,10 +10913,6 @@ static-extend@^0.1.1:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
 
-statuses@~1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
-
 statuses@~1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
@@ -11749,13 +11326,7 @@ title-case@^2.1.0:
     no-case "^2.2.0"
     upper-case "^1.0.3"
 
-tmp@0.0.31:
-  version "0.0.31"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
-  dependencies:
-    os-tmpdir "~1.0.1"
-
-tmp@0.0.x, tmp@^0.0.33:
+tmp@^0.0.33:
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
   dependencies:
@@ -11765,10 +11336,6 @@ tmpl@1.0.x:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
 
-to-array@0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
-
 to-arraybuffer@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
@@ -12036,10 +11603,6 @@ uid-number@0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
 
-ultron@1.0.x:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
-
 umask@^1.1.0, umask@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
@@ -12175,10 +11738,6 @@ urix@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
 
-url-join@^2.0.2:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
-
 url-join@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
@@ -12225,13 +11784,6 @@ user-home@^2.0.0:
   dependencies:
     os-homedir "^1.0.0"
 
-useragent@^2.1.12:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
-  dependencies:
-    lru-cache "4.1.x"
-    tmp "0.0.x"
-
 util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -12344,10 +11896,6 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
-void-elements@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
-
 vue-parser@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/vue-parser/-/vue-parser-1.1.6.tgz#3063c8431795664ebe429c23b5506899706e6355"
@@ -12492,18 +12040,6 @@ webpack-dev-middleware@3.1.3:
     url-join "^4.0.0"
     webpack-log "^1.0.1"
 
-webpack-dev-middleware@^2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-2.0.6.tgz#a51692801e8310844ef3e3790e1eacfe52326fd4"
-  dependencies:
-    loud-rejection "^1.6.0"
-    memory-fs "~0.4.1"
-    mime "^2.1.0"
-    path-is-absolute "^1.0.0"
-    range-parser "^1.0.3"
-    url-join "^2.0.2"
-    webpack-log "^1.0.1"
-
 webpack-dev-server@^3.1.0:
   version "3.1.4"
   resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.4.tgz#9a08d13c4addd1e3b6d8ace116e86715094ad5b4"
@@ -12638,7 +12174,7 @@ which-pm-runs@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
 
-which@1, which@^1.2.1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@~1.3.0:
+which@1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@~1.3.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   dependencies:
@@ -12717,13 +12253,6 @@ write@^0.2.1:
   dependencies:
     mkdirp "^0.5.1"
 
-ws@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.2.tgz#8a244fa052401e08c9886cf44a85189e1fd4067f"
-  dependencies:
-    options ">=0.0.5"
-    ultron "1.0.x"
-
 ws@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289"
@@ -12731,10 +12260,6 @@ ws@^4.0.0:
     async-limiter "~1.0.0"
     safe-buffer "~5.1.0"
 
-wtf-8@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a"
-
 xdg-basedir@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
@@ -12747,10 +12272,6 @@ xml-name-validator@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
 
-xmlhttprequest-ssl@1.5.3:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
-
 xmlhttprequest@1:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
@@ -12883,10 +12404,6 @@ yauzl@2.4.1:
   dependencies:
     fd-slicer "~1.0.1"
 
-yeast@0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
-
 yeoman-environment@^2.0.5, yeoman-environment@^2.1.1:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.2.0.tgz#6c0ee93a8d962a9f6dbc5ad4e90ae7ab34875393"

From 6225efa50ccdcd1a80fe4deeac2570deffcbac06 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 11:24:08 +0200
Subject: [PATCH 332/380] docs: update postgres provisioning

---
 docs/sources/features/datasources/postgres.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index e2dcf888025..4afde5cc6cb 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -290,4 +290,5 @@ datasources:
       password: "Password!"
     jsonData:
       sslmode: "disable" # disable/require/verify-ca/verify-full
+      timescaledb: false
 ```

From 3769df7119ca3c26220b37f68804ca03a8b32e52 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 12:16:46 +0200
Subject: [PATCH 333/380] changelog: add notes about closing #12680

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c397e45ea4..4890a471ac9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 * **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
 * **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
 * **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi)
+* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm)
 
 ### Minor
 

From a1ed3ae0943fb54c7af4ab156beaf1c883300685 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 14 Aug 2018 12:25:19 +0200
Subject: [PATCH 334/380] feat: add auto fit panels to shortcut modal, closes
 #12768

---
 public/app/core/components/help/help.ts   |  1 +
 public/app/core/services/keybindingSrv.ts | 15 +++------------
 2 files changed, 4 insertions(+), 12 deletions(-)

diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts
index a1d3c34ae5b..eac47b6e0a2 100644
--- a/public/app/core/components/help/help.ts
+++ b/public/app/core/components/help/help.ts
@@ -25,6 +25,7 @@ export class HelpCtrl {
         { keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' },
         { keys: ['d', 'E'], description: 'Expand all rows' },
         { keys: ['d', 'C'], description: 'Collapse all rows' },
+        { keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' },
         { keys: ['mod+o'], description: 'Toggle shared graph crosshair' },
       ],
       'Focused Panel': [
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index f740718063c..9d914a94a1c 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -15,14 +15,7 @@ export class KeybindingSrv {
   timepickerOpen = false;
 
   /** @ngInject */
-  constructor(
-    private $rootScope,
-    private $location,
-    private datasourceSrv,
-    private timeSrv,
-    private contextSrv,
-    private $route
-  ) {
+  constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) {
     // clear out all shortcuts on route change
     $rootScope.$on('$routeChangeSuccess', () => {
       Mousetrap.reset();
@@ -269,10 +262,8 @@ export class KeybindingSrv {
 
     //Autofit panels
     this.bind('d a', () => {
-      this.$location.search('autofitpanels', this.$location.search().autofitpanels ? null : true);
-      //Force reload
-
-      this.$route.reload();
+      // this has to be a full page reload
+      window.location.href = window.location.href + '&autofitpanels';
     });
   }
 }

From de25a4fe4ed8459c234916d39ce58cbbe5fb6669 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 12:40:07 +0200
Subject: [PATCH 335/380] docs: update

---
 .github/CONTRIBUTING.md                      |  8 ++------
 README.md                                    | 11 +++++------
 docs/sources/project/building_from_source.md | 13 ++++++-------
 3 files changed, 13 insertions(+), 19 deletions(-)

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 14c6c07ab16..769ba2a519b 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -2,15 +2,11 @@ Follow the setup guide in README.md
 
 ### Rebuild frontend assets on source change
 ```
-grunt && grunt watch
+yarn watch
 ```
 
 ### Rerun tests on source change
 ```
-npm run jest
-```
-or
-```
 yarn jest
 ```
 
@@ -21,6 +17,6 @@ test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)'
 
 ### Run tests for frontend assets before commit
 ```
-npm test
+yarn test
 go test -v ./pkg/...
 ```
diff --git a/README.md b/README.md
index 71fdb04cea6..74fb10c8066 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ To build the assets, rebuild on file change, and serve them by Grafana's webserv
 ```bash
 npm install -g yarn
 yarn install --pure-lockfile
-yarn run watch
+yarn watch
 ```
 
 Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
@@ -56,7 +56,7 @@ Note: HMR for Angular is not supported. If you edit files in the Angular part of
 
 Run tests
 ```bash
-yarn run jest
+yarn jest
 ```
 
 ### Recompile backend on source change
@@ -93,14 +93,13 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
 #### Frontend
 Execute all frontend tests
 ```bash
-yarn run test
+yarn test
 ```
 
 Writing & watching frontend tests
 
-- jest for all new tests that do not require browser context (React+more)
-   - Start watcher: `yarn run jest`
-   - Jest will run all test files that end with the name ".test.ts"
+- Start watcher: `yarn jest`
+- Jest will run all test files that end with the name ".test.ts"
 
 #### Backend
 ```bash
diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md
index 20c177211e3..08673404572 100644
--- a/docs/sources/project/building_from_source.md
+++ b/docs/sources/project/building_from_source.md
@@ -57,7 +57,7 @@ For this you need nodejs (v.6+).
 ```bash
 npm install -g yarn
 yarn install --pure-lockfile
-npm run watch
+yarn watch
 ```
 
 ## Running Grafana Locally
@@ -83,18 +83,17 @@ go get github.com/Unknwon/bra
 bra run
 ```
 
-You'll also need to run `npm run watch` to watch for changes to the front-end (typescript, html, sass)
+You'll also need to run `yarn watch` to watch for changes to the front-end (typescript, html, sass)
 
 ### Running tests
 
-- You can run backend Golang tests using "go test ./pkg/...".
-- Execute all frontend tests with "npm run test"
+- You can run backend Golang tests using `go test ./pkg/...`.
+- Execute all frontend tests with `yarn test`
 
 Writing & watching frontend tests
 
-- jest for all new tests that do not require browser context (React+more)
-   - Start watcher: `npm run jest`
-   - Jest will run all test files that end with the name ".test.ts"
+- Start watcher: `yarn jest`
+- Jest will run all test files that end with the name ".test.ts"
 
 
 ## Creating optimized release packages

From 332e59d31400f9ce250c45a121de63fc8a53ed62 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 13:42:18 +0200
Subject: [PATCH 336/380] changelog: add notes about closing #12224

[skip ci]
---
 CHANGELOG.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4890a471ac9..4bd9cb917d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,10 @@ These are new features that's still being worked on and are in an experimental p
 
 * **Dashboard**: Auto fit dashboard panels to optimize space used for current TV / Monitor [#12768](https://github.com/grafana/grafana/issues/12768)
 
+### Tech
+
+* **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224)
+
 # 5.2.2 (2018-07-25)
 
 ### Minor

From aefcb06ff823c8248f0f1ec03ce2d9578f1ea01d Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Tue, 14 Aug 2018 10:45:32 +0200
Subject: [PATCH 337/380] build: verifies the rpm packages signatures.

Closes #12370
---
 .circleci/config.yml                    |  5 +++++
 scripts/build/verify_signed_packages.sh | 17 +++++++++++++++++
 2 files changed, 22 insertions(+)
 create mode 100755 scripts/build/verify_signed_packages.sh

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 977121c30ee..c2e4cce9c4b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -147,6 +147,11 @@ jobs:
       - run:
           name: sign packages
           command: './scripts/build/sign_packages.sh'
+      - run:
+          name: verify signed packages
+          command: |
+            curl https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key
+            ./scripts/build/verify_signed_packages.sh dist/*.rpm
       - run:
           name: sha-sum packages
           command: 'go run build.go sha-dist'
diff --git a/scripts/build/verify_signed_packages.sh b/scripts/build/verify_signed_packages.sh
new file mode 100755
index 00000000000..c3e5b09afc2
--- /dev/null
+++ b/scripts/build/verify_signed_packages.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+_files=$*
+
+ALL_SIGNED=0
+
+for file in $_files; do
+  rpm -K "$file" | grep "pgp.*OK" -q
+  if [[ $? != 0 ]]; then
+    ALL_SIGNED=1
+    echo $file NOT SIGNED
+  else
+    echo $file OK
+  fi
+done
+
+
+exit $ALL_SIGNED

From 7ec146df9989e407b816b51069c8cf9bd4eb43cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= <benoit.knecht@fsfe.org>
Date: Thu, 15 Feb 2018 19:13:47 +0100
Subject: [PATCH 338/380] social: add GitLab authentication backend

GitLab could already be used as an authentication backend by properly
configuring `auth.generic_oauth`, but then there was no way to authorize
users based on their GitLab group membership.

This commit adds a `auth.gitlab` backend, similar to `auth.github`, with
an `allowed_groups` option that can be set to a list of groups whose
members should be allowed access to Grafana.
---
 conf/defaults.ini              |  12 +++
 pkg/models/models.go           |   1 +
 pkg/social/gitlab_oauth.go     | 131 +++++++++++++++++++++++++++++++++
 pkg/social/social.go           |  16 +++-
 public/app/partials/login.html |   4 +
 public/sass/_variables.scss    |   1 +
 6 files changed, 164 insertions(+), 1 deletion(-)
 create mode 100644 pkg/social/gitlab_oauth.go

diff --git a/conf/defaults.ini b/conf/defaults.ini
index 99c1537eb95..90fc144c6e0 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -270,6 +270,18 @@ api_url = https://api.github.com/user
 team_ids =
 allowed_organizations =
 
+#################################### GitLab Auth #########################
+[auth.gitlab]
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret = some_secret
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups =
+
 #################################### Google Auth #########################
 [auth.google]
 enabled = false
diff --git a/pkg/models/models.go b/pkg/models/models.go
index c2560021ee1..ba894ae591f 100644
--- a/pkg/models/models.go
+++ b/pkg/models/models.go
@@ -8,4 +8,5 @@ const (
 	TWITTER
 	GENERIC
 	GRAFANA_COM
+	GITLAB
 )
diff --git a/pkg/social/gitlab_oauth.go b/pkg/social/gitlab_oauth.go
new file mode 100644
index 00000000000..22e50b9653a
--- /dev/null
+++ b/pkg/social/gitlab_oauth.go
@@ -0,0 +1,131 @@
+package social
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"regexp"
+
+	"github.com/grafana/grafana/pkg/models"
+
+	"golang.org/x/oauth2"
+)
+
+type SocialGitlab struct {
+	*SocialBase
+	allowedDomains []string
+	allowedGroups  []string
+	apiUrl         string
+	allowSignup    bool
+}
+
+var (
+	ErrMissingGroupMembership = &Error{"User not a member of one of the required groups"}
+)
+
+func (s *SocialGitlab) Type() int {
+	return int(models.GITLAB)
+}
+
+func (s *SocialGitlab) IsEmailAllowed(email string) bool {
+	return isEmailAllowed(email, s.allowedDomains)
+}
+
+func (s *SocialGitlab) IsSignupAllowed() bool {
+	return s.allowSignup
+}
+
+func (s *SocialGitlab) IsGroupMember(client *http.Client) bool {
+	if len(s.allowedGroups) == 0 {
+		return true
+	}
+
+	for groups, url := s.GetGroups(client, s.apiUrl+"/groups"); groups != nil; groups, url = s.GetGroups(client, url) {
+		for _, allowedGroup := range s.allowedGroups {
+			for _, group := range groups {
+				if group == allowedGroup {
+					return true
+				}
+			}
+		}
+	}
+
+	return false
+}
+
+func (s *SocialGitlab) GetGroups(client *http.Client, url string) ([]string, string) {
+	type Group struct {
+		FullPath string `json:"full_path"`
+	}
+
+	var (
+		groups []Group
+		next   string
+	)
+
+	if url == "" {
+		return nil, next
+	}
+
+	response, err := HttpGet(client, url)
+	if err != nil {
+		s.log.Error("Error getting groups from GitLab API", "err", err)
+		return nil, next
+	}
+
+	if err := json.Unmarshal(response.Body, &groups); err != nil {
+		s.log.Error("Error parsing JSON from GitLab API", "err", err)
+		return nil, next
+	}
+
+	fullPaths := make([]string, len(groups))
+	for i, group := range groups {
+		fullPaths[i] = group.FullPath
+	}
+
+	if link, ok := response.Headers["Link"]; ok {
+		pattern := regexp.MustCompile(`<([^>]+)>; rel="next"`)
+		if matches := pattern.FindStringSubmatch(link[0]); matches != nil {
+			next = matches[1]
+		}
+	}
+
+	return fullPaths, next
+}
+
+func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
+
+	var data struct {
+		Id       int
+		Username string
+		Email    string
+		Name     string
+		State    string
+	}
+
+	response, err := HttpGet(client, s.apiUrl+"/user")
+	if err != nil {
+		return nil, fmt.Errorf("Error getting user info: %s", err)
+	}
+
+	err = json.Unmarshal(response.Body, &data)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting user info: %s", err)
+	}
+
+	if data.State != "active" {
+		return nil, fmt.Errorf("User %s is inactive", data.Username)
+	}
+
+	userInfo := &BasicUserInfo{
+		Name:  data.Name,
+		Login: data.Username,
+		Email: data.Email,
+	}
+
+	if !s.IsGroupMember(client) {
+		return nil, ErrMissingGroupMembership
+	}
+
+	return userInfo, nil
+}
diff --git a/pkg/social/social.go b/pkg/social/social.go
index adbe5a912d9..2be71514629 100644
--- a/pkg/social/social.go
+++ b/pkg/social/social.go
@@ -55,7 +55,7 @@ func NewOAuthService() {
 	setting.OAuthService = &setting.OAuther{}
 	setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
 
-	allOauthes := []string{"github", "google", "generic_oauth", "grafananet", "grafana_com"}
+	allOauthes := []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"}
 
 	for _, name := range allOauthes {
 		sec := setting.Raw.Section("auth." + name)
@@ -115,6 +115,20 @@ func NewOAuthService() {
 			}
 		}
 
+		// GitLab.
+		if name == "gitlab" {
+			SocialMap["gitlab"] = &SocialGitlab{
+				SocialBase: &SocialBase{
+					Config: &config,
+					log:    logger,
+				},
+				allowedDomains: info.AllowedDomains,
+				apiUrl:         info.ApiUrl,
+				allowSignup:    info.AllowSignup,
+				allowedGroups:  util.SplitString(sec.Key("allowed_groups").String()),
+			}
+		}
+
 		// Google.
 		if name == "google" {
 			SocialMap["google"] = &SocialGoogle{
diff --git a/public/app/partials/login.html b/public/app/partials/login.html
index 1919759334b..87b3cada7b5 100644
--- a/public/app/partials/login.html
+++ b/public/app/partials/login.html
@@ -51,6 +51,10 @@
             <i class="btn-service-icon fa fa-github"></i>
             Sign in with GitHub
           </a>
+          <a class="btn btn-medium btn-service btn-service--gitlab login-btn" href="login/gitlab" target="_self" ng-if="oauth.gitlab">
+            <i class="btn-service-icon fa fa-gitlab"></i>
+            Sign in with GitLab
+          </a>
           <a class="btn btn-medium btn-inverse btn-service btn-service--grafanacom login-btn" href="login/grafana_com" target="_self"
             ng-if="oauth.grafana_com">
             <i class="btn-service-icon"></i>
diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss
index 636b60c65a7..fc5b08ccff9 100644
--- a/public/sass/_variables.scss
+++ b/public/sass/_variables.scss
@@ -195,6 +195,7 @@ $tabs-padding: 10px 15px 9px;
 
 $external-services: (
     github: (bgColor: #464646, borderColor: #393939, icon: ''),
+    gitlab: (bgColor: #fc6d26, borderColor: #e24329, icon: ''),
     google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''),
     grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''),
     oauth: (bgColor: inherit, borderColor: #393939, icon: '')

From 47cb0c47fda9e045b75d25e0593c41c9b4108b30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= <benoit.knecht@fsfe.org>
Date: Tue, 20 Mar 2018 13:39:21 +0100
Subject: [PATCH 339/380] docs: document GitLab authentication backend

---
 docs/sources/installation/configuration.md | 96 ++++++++++++++++++++++
 1 file changed, 96 insertions(+)

diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index d81d8a8dcec..b82d7bed2d4 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -430,6 +430,102 @@ allowed_organizations = github google
 
 <hr>
 
+## [auth.gitlab]
+
+You need to [create a GitLab OAuth
+application](https://docs.gitlab.com/ce/integration/oauth_provider.html).
+Choose a descriptive *Name*, and use the following *Redirect URI*:
+
+```
+https://grafana.example.com/login/gitlab
+```
+
+where `https://grafana.example.com` is the URL you use to connect to Grafana.
+Adjust it as needed if you don't use HTTPS or if you use a different port; for
+instance, if you access Grafana at `http://203.0.113.31:3000`, you should use
+
+```
+http://203.0.113.31:3000/login/gitlab
+```
+
+Finally, select *api* as the *Scope* and submit the form. You'll get an
+*Application Id* and a *Secret* in return; we'll call them
+`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this
+section.
+
+Add the following to your Grafana configuration file to enable GitLab
+authentication:
+
+```ini
+[auth.gitlab]
+enabled = false
+allow_sign_up = false
+client_id = GITLAB_APPLICATION_ID
+client_secret = GITLAB_SECRET
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups =
+```
+
+Restart the Grafana backend for your changes to take effect.
+
+If you use your own instance of GitLab instead of `gitlab.com`, adjust
+`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com`
+hostname with your own.
+
+With `allow_sign_up` set to `false`, only existing users will be able to login
+using their GitLab account, but with `allow_sign_up` set to `true`, *any* user
+who can authenticate on GitLab will be able to login on your Grafana instance;
+if you use the public `gitlab.com`, it means anyone in the world would be able
+to login on your Grafana instance.
+
+You can can however limit access to only members of a given group or list of
+groups by setting the `allowed_groups` option.
+
+### allowed_groups
+
+To limit access to authenticated users that are members of one or more [GitLab
+groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups`
+to a comma- or space-separated list of groups. For instance, if you want to
+only give access to members of the `example` group, set
+
+
+```ini
+allowed_groups = example
+```
+
+If you want to also give access to members of the subgroup `bar`, which is in
+the group `foo`, set
+
+```ini
+allowed_groups = example, foo/bar
+```
+
+Note that in GitLab, the group or subgroup name doesn't always match its
+display name, especially if the display name contains spaces or special
+characters. Make sure you always use the group or subgroup name as it appears
+in the URL of the group or subgroup.
+
+Here's a complete example with `alloed_sign_up` enabled, and access limited to
+the `example` and `foo/bar` groups:
+
+```ini
+[auth.gitlab]
+enabled = false
+allow_sign_up = true
+client_id = GITLAB_APPLICATION_ID
+client_secret = GITLAB_SECRET
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_groups = example, foo/bar
+```
+
+<hr>
+
 ## [auth.google]
 
 First, you need to create a Google OAuth Client:

From ce804e9981c96e4907dd53f7b0358bf5d88ce7c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= <benoit.knecht@fsfe.org>
Date: Fri, 3 Aug 2018 15:35:04 +0200
Subject: [PATCH 340/380] social: gitlab_oauth: set user ID in case email
 changes

Set `BasicUserInfo.Id` in the value returned by
`SocialGitlab.UserInfo()`, in case the email address of the user changes
in GitLab. That way, the user association won't be lost in Grafana.
---
 pkg/social/gitlab_oauth.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pkg/social/gitlab_oauth.go b/pkg/social/gitlab_oauth.go
index 22e50b9653a..21463dabf8f 100644
--- a/pkg/social/gitlab_oauth.go
+++ b/pkg/social/gitlab_oauth.go
@@ -118,6 +118,7 @@ func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
 	}
 
 	userInfo := &BasicUserInfo{
+		Id:    fmt.Sprintf("%d", data.Id),
 		Name:  data.Name,
 		Login: data.Username,
 		Email: data.Email,

From 5a91e670d8c4156006e5baf953e15946e152a55a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= <benoit.knecht@fsfe.org>
Date: Tue, 14 Aug 2018 14:09:04 +0200
Subject: [PATCH 341/380] docs: gitlab: add note about more restrictive API
 scope

If `allowed_groups` is not used with GitLab authentication, the
*read_user* scope can be used instead of *api*.
---
 docs/sources/installation/configuration.md | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index b82d7bed2d4..75bed448a63 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -448,8 +448,12 @@ instance, if you access Grafana at `http://203.0.113.31:3000`, you should use
 http://203.0.113.31:3000/login/gitlab
 ```
 
-Finally, select *api* as the *Scope* and submit the form. You'll get an
-*Application Id* and a *Secret* in return; we'll call them
+Finally, select *api* as the *Scope* and submit the form. Note that if you're
+not going to use GitLab groups for authorization (i.e. not setting
+`allowed_groups`, see below), you can select *read_user* instead of *api* as
+the *Scope*, thus giving a more restricted access to your GitLab API.
+
+You'll get an *Application Id* and a *Secret* in return; we'll call them
 `GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this
 section.
 

From 189de8761937c222511ec9ff268f2b517b941efc Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 14:24:02 +0200
Subject: [PATCH 342/380] docs: add grafana version note for gitlab oauth

---
 docs/sources/installation/configuration.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 75bed448a63..4b14829b689 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -84,7 +84,7 @@ command line in the init.d script or the systemd service file.
 
 ### temp_data_lifetime
 
-How long temporary images in `data` directory should be kept. Defaults to: `24h`. Supported modifiers: `h` (hours), 
+How long temporary images in `data` directory should be kept. Defaults to: `24h`. Supported modifiers: `h` (hours),
 `m` (minutes), for example: `168h`, `30m`, `10h30m`. Use `0` to never clean up temporary files.
 
 ### logs
@@ -432,6 +432,8 @@ allowed_organizations = github google
 
 ## [auth.gitlab]
 
+> Only available in Grafana v5.3+.
+
 You need to [create a GitLab OAuth
 application](https://docs.gitlab.com/ce/integration/oauth_provider.html).
 Choose a descriptive *Name*, and use the following *Redirect URI*:

From e521e7b76dc8a32b4aa3b30cf2ad5f733fefda21 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Tue, 14 Aug 2018 14:24:04 +0200
Subject: [PATCH 343/380] build: fixes rpm verification.

---
 .circleci/config.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index c2e4cce9c4b..b109bb3ade5 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -150,6 +150,7 @@ jobs:
       - run:
           name: verify signed packages
           command: |
+            mkdir -p ~/.rpmdb/pubkeys
             curl https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key
             ./scripts/build/verify_signed_packages.sh dist/*.rpm
       - run:

From aed89b49c01d677bf5b41fbdc893da59f9f2868d Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 14:48:14 +0200
Subject: [PATCH 344/380] should allow one default datasource per organisation
 using provisioning

---
 .../provisioning/datasources/config_reader.go |  6 ++---
 .../datasources/config_reader_test.go         | 14 ++++++++++
 .../provisioning/datasources/datasources.go   |  2 +-
 .../testdata/multiple-org-default/config.yaml | 27 +++++++++++++++++++
 4 files changed, 45 insertions(+), 4 deletions(-)
 create mode 100644 pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml

diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go
index 4b8931f0ed3..b2930c2b679 100644
--- a/pkg/services/provisioning/datasources/config_reader.go
+++ b/pkg/services/provisioning/datasources/config_reader.go
@@ -83,7 +83,7 @@ func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*D
 }
 
 func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
-	defaultCount := 0
+	defaultCount := map[int64]int{}
 	for i := range datasources {
 		if datasources[i].Datasources == nil {
 			continue
@@ -95,8 +95,8 @@ func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
 			}
 
 			if ds.IsDefault {
-				defaultCount++
-				if defaultCount > 1 {
+				defaultCount[ds.OrgId] = defaultCount[ds.OrgId] + 1
+				if defaultCount[ds.OrgId] > 1 {
 					return ErrInvalidConfigToManyDefault
 				}
 			}
diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go
index 2e407dbe4de..df99456b21b 100644
--- a/pkg/services/provisioning/datasources/config_reader_test.go
+++ b/pkg/services/provisioning/datasources/config_reader_test.go
@@ -19,6 +19,7 @@ var (
 	allProperties                   = "testdata/all-properties"
 	versionZero                     = "testdata/version-0"
 	brokenYaml                      = "testdata/broken-yaml"
+	multipleOrgsWithDefault         = "testdata/multiple-org-default"
 
 	fakeRepo *fakeRepository
 )
@@ -73,6 +74,19 @@ func TestDatasourceAsConfig(t *testing.T) {
 			})
 		})
 
+		Convey("Multiple datasources in different organizations with is_default in each organization", func() {
+			dc := newDatasourceProvisioner(logger)
+			err := dc.applyChanges(multipleOrgsWithDefault)
+			Convey("should not raise error", func() {
+				So(err, ShouldBeNil)
+				So(len(fakeRepo.inserted), ShouldEqual, 4)
+				So(fakeRepo.inserted[0].IsDefault, ShouldBeTrue)
+				So(fakeRepo.inserted[0].OrgId, ShouldEqual, 1)
+				So(fakeRepo.inserted[2].IsDefault, ShouldBeTrue)
+				So(fakeRepo.inserted[2].OrgId, ShouldEqual, 2)
+			})
+		})
+
 		Convey("Two configured datasource and purge others ", func() {
 			Convey("two other datasources in database", func() {
 				fakeRepo.loadAll = []*models.DataSource{
diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go
index 1fa0a3b3173..de6c876baad 100644
--- a/pkg/services/provisioning/datasources/datasources.go
+++ b/pkg/services/provisioning/datasources/datasources.go
@@ -11,7 +11,7 @@ import (
 )
 
 var (
-	ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource can be marked as default")
+	ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource per organization can be marked as default")
 )
 
 func Provision(configDirectory string) error {
diff --git a/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml b/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
new file mode 100644
index 00000000000..447317a8c77
--- /dev/null
+++ b/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
@@ -0,0 +1,27 @@
+apiVersion: 1
+
+datasources:
+  - orgId: 1
+    name: prometheus
+    type: prometheus
+    isDefault: True
+    access: proxy
+    url: http://prometheus.example.com:9090
+  - name: Graphite
+    type: graphite
+    access: proxy
+    url: http://localhost:8080
+    is_default: true
+  - orgId: 2
+    name: prometheus
+    type: prometheus
+    isDefault: True
+    access: proxy
+    url: http://prometheus.example.com:9090
+  - orgId: 2
+    name: Graphite
+    type: graphite
+    access: proxy
+    url: http://localhost:8080
+    is_default: true
+

From 570d2fede374ea72f6294b88d9d45e34d09f5083 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Tue, 14 Aug 2018 14:48:26 +0200
Subject: [PATCH 345/380] build: cleanup

---
 .circleci/config.yml            | 2 +-
 scripts/circle-test-frontend.sh | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index b109bb3ade5..1e046aec34d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -151,7 +151,7 @@ jobs:
           name: verify signed packages
           command: |
             mkdir -p ~/.rpmdb/pubkeys
-            curl https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key
+            curl -s https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key
             ./scripts/build/verify_signed_packages.sh dist/*.rpm
       - run:
           name: sha-sum packages
diff --git a/scripts/circle-test-frontend.sh b/scripts/circle-test-frontend.sh
index 9857e00f70d..796af82e7d8 100755
--- a/scripts/circle-test-frontend.sh
+++ b/scripts/circle-test-frontend.sh
@@ -11,7 +11,6 @@ function exit_if_fail {
 }
 
 exit_if_fail npm run test:coverage
-exit_if_fail npm run build
 
 # publish code coverage
 echo "Publishing javascript code coverage"

From ad1cf6c2b80ddc85e02d933740c3b7f1c090fb18 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 14:55:07 +0200
Subject: [PATCH 346/380] changelog: add notes about closing #5623

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4bd9cb917d7..34d3c82916e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 # 5.3.0 (unreleased)
 
+* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht)
 * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
 * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
 * **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)

From c5c518fd177459a7a2711d28c01c1f4c162e7edd Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 14 Aug 2018 15:17:29 +0200
Subject: [PATCH 347/380] docs: remove message property in response from get
 alerts http api

Fixes #12524
---
 docs/sources/http_api/alerting.md | 1 -
 1 file changed, 1 deletion(-)

diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md
index e4fe0dad3ff..80b6e283be3 100644
--- a/docs/sources/http_api/alerting.md
+++ b/docs/sources/http_api/alerting.md
@@ -59,7 +59,6 @@ Content-Type: application/json
     "panelId": 1,
     "name": "fire place sensor",
     "state": "alerting",
-    "message": "Someone is trying to break in through the fire place",
     "newStateDate": "2018-05-14T05:55:20+02:00",
     "evalDate": "0001-01-01T00:00:00Z",
     "evalData": null,

From 6a8b1e14cc9620b586273f47175b4c30f0f7079d Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Tue, 14 Aug 2018 16:37:38 +0200
Subject: [PATCH 348/380] docs: cloudwatch dimensions reference link.

---
 docs/sources/features/datasources/cloudwatch.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md
index d178c176602..7adc6ebe4fb 100644
--- a/docs/sources/features/datasources/cloudwatch.md
+++ b/docs/sources/features/datasources/cloudwatch.md
@@ -115,6 +115,8 @@ and `dimension keys/values`.
 In place of `region` you can specify `default` to use the default region configured in the datasource for the query,
 e.g. `metrics(AWS/DynamoDB, default)` or `dimension_values(default, ..., ..., ...)`.
 
+Read more about the available dimensions in the [CloudWatch  Metrics and Dimensions Reference](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CW_Support_For_AWS.html).
+
 Name | Description
 ------- | --------
 *regions()* | Returns a list of regions AWS provides their service.

From 13921902b5bd988735967697d103f2bb4ee10293 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Wed, 15 Aug 2018 09:46:59 +0200
Subject: [PATCH 349/380] Set User-Agent header in all proxied datasource
 requests

Header value will be Grafana/%version%, i.e. Grafana/5.3.0
---
 pkg/api/pluginproxy/ds_proxy.go      |  2 +-
 pkg/api/pluginproxy/ds_proxy_test.go | 15 ++++++++-------
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go
index 74ad4e226fd..c8056040d24 100644
--- a/pkg/api/pluginproxy/ds_proxy.go
+++ b/pkg/api/pluginproxy/ds_proxy.go
@@ -203,7 +203,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
 		req.Header.Del("X-Forwarded-Host")
 		req.Header.Del("X-Forwarded-Port")
 		req.Header.Del("X-Forwarded-Proto")
-		req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s Proxied-DS-Request %s", setting.BuildVersion, proxy.ds.Type))
+		req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
 
 		// set X-Forwarded-For header
 		if req.RemoteAddr != "" {
diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go
index 9b768c3d32a..ad331113f46 100644
--- a/pkg/api/pluginproxy/ds_proxy_test.go
+++ b/pkg/api/pluginproxy/ds_proxy_test.go
@@ -212,20 +212,21 @@ func TestDSRouteRule(t *testing.T) {
 		})
 
 		Convey("When proxying graphite", func() {
+			setting.BuildVersion = "5.3.0"
 			plugin := &plugins.DataSourcePlugin{}
 			ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
 			ctx := &m.ReqContext{}
 
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
+			req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+			So(err, ShouldBeNil)
 
-			requestURL, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestURL, Header: http.Header{}}
-
-			proxy.getDirector()(&req)
+			proxy.getDirector()(req)
 
 			Convey("Can translate request url and path", func() {
 				So(req.URL.Host, ShouldEqual, "graphite:8080")
 				So(req.URL.Path, ShouldEqual, "/render")
+				So(req.Header.Get("User-Agent"), ShouldEqual, "Grafana/5.3.0")
 			})
 		})
 
@@ -243,10 +244,10 @@ func TestDSRouteRule(t *testing.T) {
 			ctx := &m.ReqContext{}
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 
-			requestURL, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestURL, Header: http.Header{}}
+			req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+			So(err, ShouldBeNil)
 
-			proxy.getDirector()(&req)
+			proxy.getDirector()(req)
 
 			Convey("Should add db to url", func() {
 				So(req.URL.Path, ShouldEqual, "/db/site/")

From 713fac8e7810f886aff8b511f9df8c33fdb5fac9 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Wed, 15 Aug 2018 10:32:17 +0200
Subject: [PATCH 350/380] build: duplicate docker run-script removed.

---
 scripts/docker/run.sh | 67 -------------------------------------------
 1 file changed, 67 deletions(-)
 delete mode 100755 scripts/docker/run.sh

diff --git a/scripts/docker/run.sh b/scripts/docker/run.sh
deleted file mode 100755
index df64ce3adf4..00000000000
--- a/scripts/docker/run.sh
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/bin/bash -e
-
-PERMISSIONS_OK=0
-
-if [ ! -r "$GF_PATHS_CONFIG" ]; then
-    echo "GF_PATHS_CONFIG='$GF_PATHS_CONFIG' is not readable."
-    PERMISSIONS_OK=1
-fi
-
-if [ ! -w "$GF_PATHS_DATA" ]; then
-    echo "GF_PATHS_DATA='$GF_PATHS_DATA' is not writable."
-    PERMISSIONS_OK=1
-fi
-
-if [ ! -r "$GF_PATHS_HOME" ]; then
-    echo "GF_PATHS_HOME='$GF_PATHS_HOME' is not readable."
-    PERMISSIONS_OK=1
-fi
-
-if [ $PERMISSIONS_OK -eq 1 ]; then
-    echo "You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later"
-fi
-
-if [ ! -d "$GF_PATHS_PLUGINS" ]; then
-    mkdir "$GF_PATHS_PLUGINS"
-fi
-
-
-if [ ! -z ${GF_AWS_PROFILES+x} ]; then
-    > "$GF_PATHS_HOME/.aws/credentials"
-
-    for profile in ${GF_AWS_PROFILES}; do
-        access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID"
-        secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY"
-        region_varname="GF_AWS_${profile}_REGION"
-
-        if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then
-            echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials"
-            echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
-            echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
-            if [ ! -z "${!region_varname}" ]; then
-                echo "region = ${!region_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
-            fi
-        fi
-    done
-
-    chmod 600 "$GF_PATHS_HOME/.aws/credentials"
-fi
-
-if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then
-  OLDIFS=$IFS
-  IFS=','
-  for plugin in ${GF_INSTALL_PLUGINS}; do
-    IFS=$OLDIFS
-    grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}
-  done
-fi
-
-exec grafana-server                                         \
-  --homepath="$GF_PATHS_HOME"                               \
-  --config="$GF_PATHS_CONFIG"                               \
-  "$@"                                                      \
-  cfg:default.log.mode="console"                            \
-  cfg:default.paths.data="$GF_PATHS_DATA"                   \
-  cfg:default.paths.logs="$GF_PATHS_LOGS"                   \
-  cfg:default.paths.plugins="$GF_PATHS_PLUGINS"             \
-  cfg:default.paths.provisioning="$GF_PATHS_PROVISIONING"
\ No newline at end of file

From d244b59cc19f28bf5ccd7a7da4a89ba9ce83c2bd Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Thu, 16 Aug 2018 13:15:50 +0200
Subject: [PATCH 351/380] docs: docker and restarts.

Closes #10784
---
 docs/sources/installation/docker.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md
index 1f755625699..2a6af952a2b 100644
--- a/docs/sources/installation/docker.md
+++ b/docs/sources/installation/docker.md
@@ -38,6 +38,8 @@ The back-end web server has a number of configuration options. Go to the
 [Configuration]({{< relref "configuration.md" >}}) page for details on all
 those options.
 
+> For any changes to `conf/grafana.ini` (or corresponding environment variables) to take effect you need to restart Grafana by restarting the Docker container.
+
 ## Running a Specific Version of Grafana
 
 ```bash

From 0ce8a6a69d11dfb53415e16cf789d7598a52f219 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Thu, 16 Aug 2018 13:32:21 +0200
Subject: [PATCH 352/380] docs: cleanup.

---
 docs/sources/installation/docker.md | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md
index 2a6af952a2b..6bf25ad8232 100644
--- a/docs/sources/installation/docker.md
+++ b/docs/sources/installation/docker.md
@@ -51,10 +51,13 @@ $ docker run \
   grafana/grafana:5.1.0
 ```
 
-## Running of the master branch
+## Running the master branch
 
-For every successful commit we publish a Grafana container to [`grafana/grafana`](https://hub.docker.com/r/grafana/grafana/tags/) and [`grafana/grafana-dev`](https://hub.docker.com/r/grafana/grafana-dev/tags/). In `grafana/grafana` container we will always overwrite the `master` tag with the latest version. In `grafana/grafana-dev` we will include
-the git commit in the tag. If you run Grafana master in production we **strongly** recommend that you use the later since different machines might run different version of grafana if they pull the master tag at different times.
+For every successful build of the master branch we update the `grafana/grafana:master` tag and create a new tag `grafana/grafana-dev:master-<commit hash>` with the hash of the git commit that was built. This means you can always get the latest version of Grafana.
+
+When running Grafana master in production we **strongly** recommend that you use the `grafana/grafana-dev:master-<commit hash>` tag as that will guarantee that you use a specific version of Grafana instead of whatever was the most recent commit at the time.
+
+For a list of available tags, check out [grafana/grafana](https://hub.docker.com/r/grafana/grafana/tags/) and [grafana/grafana-dev](https://hub.docker.com/r/grafana/grafana-dev/tags/). 
 
 ## Installing Plugins for Grafana
 

From cc50cfd9d37bfda4e355e82e322af0659ca7e4fd Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Thu, 16 Aug 2018 11:14:12 +0200
Subject: [PATCH 353/380] build: beta versions no longer tagged as latest.

Closes #12862
---
 packaging/docker/push_to_docker_hub.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh
index 3cf97d580ca..526c216f8fa 100755
--- a/packaging/docker/push_to_docker_hub.sh
+++ b/packaging/docker/push_to_docker_hub.sh
@@ -15,10 +15,10 @@ fi
 echo "pushing ${_docker_repo}:${_grafana_version}"
 docker push "${_docker_repo}:${_grafana_version}"
 
-if echo "$_grafana_tag" | grep -q "^v"; then
+if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
 	echo "pushing ${_docker_repo}:latest"
 	docker push "${_docker_repo}:latest"
-else
+elif echo "$_grafana_tag" | grep -q "master"; then
 	echo "pushing grafana/grafana:master"
 	docker push grafana/grafana:master
 fi

From 04d50fb4054d7bb17a94cfa7b5cf34ef2a9c4a6b Mon Sep 17 00:00:00 2001
From: Pierre GIRAUD <pierre.giraud@gmail.com>
Date: Thu, 16 Aug 2018 15:19:07 +0200
Subject: [PATCH 354/380] Doc - fix broken link

---
 docs/sources/guides/basic_concepts.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md
index b710a227a79..d3f8dd0ba63 100644
--- a/docs/sources/guides/basic_concepts.md
+++ b/docs/sources/guides/basic_concepts.md
@@ -54,7 +54,7 @@ We utilize a unit abstraction so that Grafana looks great on all screens both sm
 
  > Note: With MaxDataPoint functionality, Grafana can show you the perfect amount of datapoints no matter your resolution or time-range.
 
-Utilize the [Repeating Row functionality](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) to dynamically create or remove entire Rows (that can be filled with Panels), based on the Template variables selected.
+Utilize the [Repeating Rows functionality](/reference/templating/#repeating-rows) to dynamically create or remove entire Rows (that can be filled with Panels), based on the Template variables selected.
 
 Rows can be collapsed by clicking on the Row Title. If you save a Dashboard with a Row collapsed, it will save in that state and will not preload those graphs until the row is expanded.
 
@@ -72,7 +72,7 @@ Panels like the [Graph](/reference/graph/) panel allow you to graph as many metr
 
 Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/templating/) variable strings within the panel configuration (including queries to your Data Source configured via the Query Editor).
 
-Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected.
+Utilize the [Repeating Panel](/reference/templating/#repeating-panels) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#repeating-panels) selected.
 
 The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift).
 

From ea704fcf9bb03b45d84edd5013a74b8f7e988844 Mon Sep 17 00:00:00 2001
From: Pierre GIRAUD <pierre.giraud@dalibo.com>
Date: Thu, 16 Aug 2018 16:49:04 +0200
Subject: [PATCH 355/380] Update doc about repeating panels

There's no per-row contextual menu anymore
Instead there's a  option
---
 docs/sources/reference/templating.md | 26 ++++++++++++++++++++------
 1 file changed, 20 insertions(+), 6 deletions(-)

diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index d04d56dc788..96109503e12 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -284,18 +284,32 @@ Currently only supported for Prometheus data sources. This variable represents t
 Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want
 Grafana to dynamically create new panels or rows based on what values you have selected you can use the *Repeat* feature.
 
-If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel or one row and have Grafana repeat that row
-for every selected value. You find this option under the General tab in panel edit mode. Select the variable to repeat by, and a `min span`.
-The `min span` controls how small Grafana will make the panels (if you have many values selected). Grafana will automatically adjust the width of
-each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated panel.
+If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel and have Grafana repeat that panel
+for every selected value. You find the *Repeat* feature under the *General tab* in panel edit mode.
+
+The `direction` controls how the panels will be arranged.
+
+By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width
+of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated
+panel. Each panel will never be smaller that the provided `Min width` if you have many selected values.
+
+By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
 
 Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
 You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
 
 ## Repeating Rows
 
-This option requires you to open the row options view. Hover over the row left side to trigger the row menu, in this menu click `Row Options`. This
-opens the row options view. Here you find a *Repeat* dropdown where you can select the variable to repeat by.
+As seen above with the *Panels* you can also repeat *Rows* if you have variables set with  `Multi-value` or
+`Include all value` selection option.
+
+To enable this feature you need to first add a new *Row* using the *Add Panel* menu. Then by hovering the row title and
+clicking on the cog button, you will access the `Row Options` configuration panel. You can then select the variable
+you want to repeat the row for.
+
+It may be a good idea to use a variable in the row title as well.
+
+Example: [Repeated Rows Dashboard](http://play.grafana.org/dashboard/db/repeated-rows)
 
 ### URL state
 

From d06c8c0e6816bd255863ee3efe4952574d6a0292 Mon Sep 17 00:00:00 2001
From: Pierre GIRAUD <pierre.giraud@dalibo.com>
Date: Fri, 17 Aug 2018 09:31:30 +0200
Subject: [PATCH 356/380] Doc - fix title level

---
 docs/sources/reference/templating.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index 96109503e12..7f86465312c 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -311,11 +311,11 @@ It may be a good idea to use a variable in the row title as well.
 
 Example: [Repeated Rows Dashboard](http://play.grafana.org/dashboard/db/repeated-rows)
 
-### URL state
+## URL state
 
 Variable values are always synced to the URL using the syntax `var-<varname>=value`.
 
-### Examples
+## Examples
 
 - [Graphite Templated Dashboard](http://play.grafana.org/dashboard/db/graphite-templated-nested)
 - [Elasticsearch Templated Dashboard](http://play.grafana.org/dashboard/db/elasticsearch-templated)

From 190432296d3e1b936d3ac3584bf084c8556930b5 Mon Sep 17 00:00:00 2001
From: Patrick O'Carroll <ocarroll.patrick@gmail.com>
Date: Fri, 17 Aug 2018 10:40:57 +0200
Subject: [PATCH 357/380] removed inverse btn styling and added bgColor to
 generic oauth and grafana.com login buttons, added styling so log in button
 uses dark theme inverse btn styling both for dark and light theme

---
 public/app/partials/login.html | 4 ++--
 public/sass/_variables.scss    | 4 ++--
 public/sass/pages/_login.scss  | 9 +++++++++
 3 files changed, 13 insertions(+), 4 deletions(-)

diff --git a/public/app/partials/login.html b/public/app/partials/login.html
index 87b3cada7b5..656103adce2 100644
--- a/public/app/partials/login.html
+++ b/public/app/partials/login.html
@@ -55,12 +55,12 @@
             <i class="btn-service-icon fa fa-gitlab"></i>
             Sign in with GitLab
           </a>
-          <a class="btn btn-medium btn-inverse btn-service btn-service--grafanacom login-btn" href="login/grafana_com" target="_self"
+          <a class="btn btn-medium btn-service btn-service--grafanacom login-btn" href="login/grafana_com" target="_self"
             ng-if="oauth.grafana_com">
             <i class="btn-service-icon"></i>
             Sign in with Grafana.com
           </a>
-          <a class="btn btn-medium btn-inverse btn-service btn-service--oauth login-btn" href="login/generic_oauth" target="_self"
+          <a class="btn btn-medium btn-service btn-service--oauth login-btn" href="login/generic_oauth" target="_self"
             ng-if="oauth.generic_oauth">
             <i class="btn-service-icon fa fa-sign-in"></i>
             Sign in with {{oauth.generic_oauth.name}}
diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss
index fc5b08ccff9..841f06156ac 100644
--- a/public/sass/_variables.scss
+++ b/public/sass/_variables.scss
@@ -197,7 +197,7 @@ $external-services: (
     github: (bgColor: #464646, borderColor: #393939, icon: ''),
     gitlab: (bgColor: #fc6d26, borderColor: #e24329, icon: ''),
     google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''),
-    grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''),
-    oauth: (bgColor: inherit, borderColor: #393939, icon: '')
+    grafanacom: (bgColor: #262628, borderColor: #393939, icon: ''),
+    oauth: (bgColor: #262628, borderColor: #393939, icon: '')
   )
   !default;
diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss
index 9a4260576b1..8e5c8f33e37 100644
--- a/public/sass/pages/_login.scss
+++ b/public/sass/pages/_login.scss
@@ -76,6 +76,15 @@ select:-webkit-autofill:focus {
       margin-left: 1rem;
     }
   }
+  & .btn-inverse {
+    color: #e3e3e3;
+    text-shadow: 0px 1px 0 rgba(0, 0, 0, 0.1);
+    background-color: #2a2a2c;
+    background-image: linear-gradient(to bottom, #262628, #303032);
+    background-repeat: repeat-x;
+    border-color: #262628;
+    box-shadow: -1px -1px 0 0 rgba(255, 255, 255, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3);
+  }
 }
 
 .login-button-forgot-password {

From d6ad1ced6d88ee944e2e04d33a6d3405a54ae5df Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 17 Aug 2018 12:20:21 +0200
Subject: [PATCH 358/380] when value in variable changes, identify which
 variable(s) to update

Given you have variables a, b, c, d where b depends on a, c depends on b, c, d depends on a.
When updating a only an update of b and d should be triggered since c depends on b
and c will be updated eventually when the update of b are finished.
---
 public/app/core/utils/dag.test.ts             | 108 ++++++++++
 public/app/core/utils/dag.ts                  | 201 ++++++++++++++++++
 .../app/features/templating/variable_srv.ts   |  38 +++-
 3 files changed, 337 insertions(+), 10 deletions(-)
 create mode 100644 public/app/core/utils/dag.test.ts
 create mode 100644 public/app/core/utils/dag.ts

diff --git a/public/app/core/utils/dag.test.ts b/public/app/core/utils/dag.test.ts
new file mode 100644
index 00000000000..a89ab27cda3
--- /dev/null
+++ b/public/app/core/utils/dag.test.ts
@@ -0,0 +1,108 @@
+import { Graph } from './dag';
+
+describe('Directed acyclic graph', () => {
+  describe('Given a graph with nodes with different links in between them', () => {
+    let dag = new Graph();
+    let nodeA = dag.createNode('A');
+    let nodeB = dag.createNode('B');
+    let nodeC = dag.createNode('C');
+    let nodeD = dag.createNode('D');
+    let nodeE = dag.createNode('E');
+    let nodeF = dag.createNode('F');
+    let nodeG = dag.createNode('G');
+    let nodeH = dag.createNode('H');
+    let nodeI = dag.createNode('I');
+    dag.link([nodeB, nodeC, nodeD, nodeE, nodeF, nodeG, nodeH], nodeA);
+    dag.link([nodeC, nodeD, nodeE, nodeF, nodeI], nodeB);
+    dag.link([nodeD, nodeE, nodeF, nodeG], nodeC);
+    dag.link([nodeE, nodeF], nodeD);
+    dag.link([nodeF, nodeG], nodeE);
+    //printGraph(dag);
+
+    it('nodes in graph should have expected edges', () => {
+      expect(nodeA.inputEdges).toHaveLength(7);
+      expect(nodeA.outputEdges).toHaveLength(0);
+      expect(nodeA.edges).toHaveLength(7);
+
+      expect(nodeB.inputEdges).toHaveLength(5);
+      expect(nodeB.outputEdges).toHaveLength(1);
+      expect(nodeB.edges).toHaveLength(6);
+
+      expect(nodeC.inputEdges).toHaveLength(4);
+      expect(nodeC.outputEdges).toHaveLength(2);
+      expect(nodeC.edges).toHaveLength(6);
+
+      expect(nodeD.inputEdges).toHaveLength(2);
+      expect(nodeD.outputEdges).toHaveLength(3);
+      expect(nodeD.edges).toHaveLength(5);
+
+      expect(nodeE.inputEdges).toHaveLength(2);
+      expect(nodeE.outputEdges).toHaveLength(4);
+      expect(nodeE.edges).toHaveLength(6);
+
+      expect(nodeF.inputEdges).toHaveLength(0);
+      expect(nodeF.outputEdges).toHaveLength(5);
+      expect(nodeF.edges).toHaveLength(5);
+
+      expect(nodeG.inputEdges).toHaveLength(0);
+      expect(nodeG.outputEdges).toHaveLength(3);
+      expect(nodeG.edges).toHaveLength(3);
+
+      expect(nodeH.inputEdges).toHaveLength(0);
+      expect(nodeH.outputEdges).toHaveLength(1);
+      expect(nodeH.edges).toHaveLength(1);
+
+      expect(nodeI.inputEdges).toHaveLength(0);
+      expect(nodeI.outputEdges).toHaveLength(1);
+      expect(nodeI.edges).toHaveLength(1);
+
+      expect(nodeA.getEdgeFrom(nodeB)).not.toBeUndefined();
+      expect(nodeB.getEdgeTo(nodeA)).not.toBeUndefined();
+    });
+
+    it('when optimizing input edges for node A should return node B and H', () => {
+      const actual = nodeA.getOptimizedInputEdges().map(e => e.inputNode);
+      expect(actual).toHaveLength(2);
+      expect(actual).toEqual(expect.arrayContaining([nodeB, nodeH]));
+    });
+
+    it('when optimizing input edges for node B should return node C', () => {
+      const actual = nodeB.getOptimizedInputEdges().map(e => e.inputNode);
+      expect(actual).toHaveLength(2);
+      expect(actual).toEqual(expect.arrayContaining([nodeC, nodeI]));
+    });
+
+    it('when optimizing input edges for node C should return node D', () => {
+      const actual = nodeC.getOptimizedInputEdges().map(e => e.inputNode);
+      expect(actual).toHaveLength(1);
+      expect(actual).toEqual(expect.arrayContaining([nodeD]));
+    });
+
+    it('when optimizing input edges for node D should return node E', () => {
+      const actual = nodeD.getOptimizedInputEdges().map(e => e.inputNode);
+      expect(actual).toHaveLength(1);
+      expect(actual).toEqual(expect.arrayContaining([nodeE]));
+    });
+
+    it('when optimizing input edges for node E should return node F and G', () => {
+      const actual = nodeE.getOptimizedInputEdges().map(e => e.inputNode);
+      expect(actual).toHaveLength(2);
+      expect(actual).toEqual(expect.arrayContaining([nodeF, nodeG]));
+    });
+
+    it('when optimizing input edges for node F should return zero nodes', () => {
+      const actual = nodeF.getOptimizedInputEdges();
+      expect(actual).toHaveLength(0);
+    });
+
+    it('when optimizing input edges for node G should return zero nodes', () => {
+      const actual = nodeG.getOptimizedInputEdges();
+      expect(actual).toHaveLength(0);
+    });
+
+    it('when optimizing input edges for node H should return zero nodes', () => {
+      const actual = nodeH.getOptimizedInputEdges();
+      expect(actual).toHaveLength(0);
+    });
+  });
+});
diff --git a/public/app/core/utils/dag.ts b/public/app/core/utils/dag.ts
new file mode 100644
index 00000000000..1d61280fb05
--- /dev/null
+++ b/public/app/core/utils/dag.ts
@@ -0,0 +1,201 @@
+export class Edge {
+  inputNode: Node;
+  outputNode: Node;
+
+  _linkTo(node, direction) {
+    if (direction <= 0) {
+      node.inputEdges.push(this);
+    }
+
+    if (direction >= 0) {
+      node.outputEdges.push(this);
+    }
+
+    node.edges.push(this);
+  }
+
+  link(inputNode: Node, outputNode: Node) {
+    this.unlink();
+    this.inputNode = inputNode;
+    this.outputNode = outputNode;
+
+    this._linkTo(inputNode, 1);
+    this._linkTo(outputNode, -1);
+    return this;
+  }
+
+  unlink() {
+    let pos;
+    let inode = this.inputNode;
+    let onode = this.outputNode;
+
+    if (!(inode && onode)) {
+      return;
+    }
+
+    pos = inode.edges.indexOf(this);
+    if (pos > -1) {
+      inode.edges.splice(pos, 1);
+    }
+
+    pos = onode.edges.indexOf(this);
+    if (pos > -1) {
+      onode.edges.splice(pos, 1);
+    }
+
+    pos = inode.outputEdges.indexOf(this);
+    if (pos > -1) {
+      inode.outputEdges.splice(pos, 1);
+    }
+
+    pos = onode.inputEdges.indexOf(this);
+    if (pos > -1) {
+      onode.inputEdges.splice(pos, 1);
+    }
+
+    this.inputNode = null;
+    this.outputNode = null;
+  }
+}
+
+export class Node {
+  name: string;
+  edges: Edge[];
+  inputEdges: Edge[];
+  outputEdges: Edge[];
+
+  constructor(name: string) {
+    this.name = name;
+    this.edges = [];
+    this.inputEdges = [];
+    this.outputEdges = [];
+  }
+
+  getEdgeFrom(from: string | Node): Edge {
+    if (!from) {
+      return null;
+    }
+
+    if (typeof from === 'object') {
+      return this.inputEdges.find(e => e.inputNode.name === from.name);
+    }
+
+    return this.inputEdges.find(e => e.inputNode.name === from);
+  }
+
+  getEdgeTo(to: string | Node): Edge {
+    if (!to) {
+      return null;
+    }
+
+    if (typeof to === 'object') {
+      return this.outputEdges.find(e => e.outputNode.name === to.name);
+    }
+
+    return this.outputEdges.find(e => e.outputNode.name === to);
+  }
+
+  getOptimizedInputEdges(): Edge[] {
+    let toBeRemoved = [];
+    this.inputEdges.forEach(e => {
+      let inputEdgesNodes = e.inputNode.inputEdges.map(e => e.inputNode);
+
+      inputEdgesNodes.forEach(n => {
+        let edgeToRemove = n.getEdgeTo(this.name);
+        if (edgeToRemove) {
+          toBeRemoved.push(edgeToRemove);
+        }
+      });
+    });
+
+    return this.inputEdges.filter(e => toBeRemoved.indexOf(e) === -1);
+  }
+}
+
+export class Graph {
+  nodes = {};
+
+  constructor() {}
+
+  createNode(name: string): Node {
+    const n = new Node(name);
+    this.nodes[name] = n;
+    return n;
+  }
+
+  createNodes(names: string[]): Node[] {
+    let nodes = [];
+    names.forEach(name => {
+      nodes.push(this.createNode(name));
+    });
+    return nodes;
+  }
+
+  link(input: string | string[] | Node | Node[], output: string | string[] | Node | Node[]): Edge[] {
+    let inputArr = [];
+    let outputArr = [];
+    let inputNodes = [];
+    let outputNodes = [];
+
+    if (input instanceof Array) {
+      inputArr = input;
+    } else {
+      inputArr = [input];
+    }
+
+    if (output instanceof Array) {
+      outputArr = output;
+    } else {
+      outputArr = [output];
+    }
+
+    for (let n = 0; n < inputArr.length; n++) {
+      const i = inputArr[n];
+      if (typeof i === 'string') {
+        inputNodes.push(this.getNode(i));
+      } else {
+        inputNodes.push(i);
+      }
+    }
+
+    for (let n = 0; n < outputArr.length; n++) {
+      const i = outputArr[n];
+      if (typeof i === 'string') {
+        outputNodes.push(this.getNode(i));
+      } else {
+        outputNodes.push(i);
+      }
+    }
+
+    let edges = [];
+    inputNodes.forEach(input => {
+      outputNodes.forEach(output => {
+        edges.push(this.createEdge().link(input, output));
+      });
+    });
+    return edges;
+  }
+
+  createEdge(): Edge {
+    return new Edge();
+  }
+
+  getNode(name: string): Node {
+    return this.nodes[name];
+  }
+}
+
+export const printGraph = (g: Graph) => {
+  Object.keys(g.nodes).forEach(name => {
+    const n = g.nodes[name];
+    let outputEdges = n.outputEdges.map(e => e.outputNode.name).join(', ');
+    if (!outputEdges) {
+      outputEdges = '<none>';
+    }
+    let inputEdges = n.inputEdges.map(e => e.inputNode.name).join(', ');
+    if (!inputEdges) {
+      inputEdges = '<none>';
+    }
+    console.log(`${n.name}:\n - links to:   ${outputEdges}\n - links from: ${inputEdges}`);
+  });
+};
diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts
index 8ad3c2845e2..bd214639552 100644
--- a/public/app/features/templating/variable_srv.ts
+++ b/public/app/features/templating/variable_srv.ts
@@ -2,6 +2,7 @@ import angular from 'angular';
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
 import { variableTypes } from './variable';
+import { Graph } from 'app/core/utils/dag';
 
 export class VariableSrv {
   dashboard: any;
@@ -120,16 +121,13 @@ export class VariableSrv {
       return this.$q.when();
     }
 
-    // cascade updates to variables that use this variable
-    var promises = _.map(this.variables, otherVariable => {
-      if (otherVariable === variable) {
-        return;
-      }
-
-      if (otherVariable.dependsOn(variable)) {
-        return this.updateOptions(otherVariable);
-      }
-    });
+    const g = this.createGraph();
+    const promises = g
+      .getNode(variable.name)
+      .getOptimizedInputEdges()
+      .map(e => {
+        return this.updateOptions(this.variables.find(v => v.name === e.inputNode.name));
+      });
 
     return this.$q.all(promises).then(() => {
       if (emitChangeEvents) {
@@ -288,6 +286,26 @@ export class VariableSrv {
     filter.operator = options.operator;
     this.variableUpdated(variable, true);
   }
+
+  createGraph() {
+    let g = new Graph();
+
+    this.variables.forEach(v1 => {
+      g.createNode(v1.name);
+
+      this.variables.forEach(v2 => {
+        if (v1 === v2) {
+          return;
+        }
+
+        if (v1.dependsOn(v2)) {
+          g.link(v1.name, v2.name);
+        }
+      });
+    });
+
+    return g;
+  }
 }
 
 coreModule.service('variableSrv', VariableSrv);

From 492f5cac92e2305ec0e58fc27b6e309704bcb70f Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 17 Aug 2018 14:26:28 +0200
Subject: [PATCH 359/380] devenv: update sql dashboards

---
 .../datasource_tests_mssql_unittest.json      | 224 ++++++++++++++++--
 .../datasource_tests_mysql_unittest.json      | 214 +++++++++++++++--
 .../datasource_tests_postgres_unittest.json   | 202 ++++++++++++++--
 3 files changed, 593 insertions(+), 47 deletions(-)

diff --git a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
index 0d291f01a09..b2d757ae188 100644
--- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1533713720618,
+  "iteration": 1534507501976,
   "links": [],
   "panels": [
     {
@@ -1197,6 +1197,196 @@
         "x": 0,
         "y": 27
       },
+      "id": 38,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(timeInt32, '$summarize'), \n  measurement as metric, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(timeInt32) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__unixEpochGroup(timeInt32, '$summarize'), \n  measurement \nORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mssql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 27
+      },
+      "id": 39,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "MovingAverageValueOne",
+          "dashes": true,
+          "lines": false
+        },
+        {
+          "alias": "MovingAverageValueTwo",
+          "dashes": true,
+          "lines": false,
+          "yaxis": 1
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(timeInt32, '$summarize'), \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(timeInt32) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__unixEpochGroup(timeInt32, '$summarize')\nORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  time,\n  avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n  avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mssql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 35
+      },
       "id": 4,
       "legend": {
         "alignAsTable": true,
@@ -1282,7 +1472,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 27
+        "y": 35
       },
       "id": 28,
       "legend": {
@@ -1367,7 +1557,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 35
+        "y": 43
       },
       "id": 19,
       "legend": {
@@ -1454,7 +1644,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 35
+        "y": 43
       },
       "id": 18,
       "legend": {
@@ -1539,7 +1729,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 43
+        "y": 51
       },
       "id": 17,
       "legend": {
@@ -1626,7 +1816,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 43
+        "y": 51
       },
       "id": 20,
       "legend": {
@@ -1711,7 +1901,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 51
+        "y": 59
       },
       "id": 29,
       "legend": {
@@ -1798,7 +1988,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 51
+        "y": 59
       },
       "id": 30,
       "legend": {
@@ -1885,7 +2075,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 59
+        "y": 67
       },
       "id": 14,
       "legend": {
@@ -1973,7 +2163,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 59
+        "y": 67
       },
       "id": 15,
       "legend": {
@@ -2060,7 +2250,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 67
+        "y": 75
       },
       "id": 25,
       "legend": {
@@ -2148,7 +2338,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 67
+        "y": 75
       },
       "id": 22,
       "legend": {
@@ -2235,7 +2425,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 75
+        "y": 83
       },
       "id": 21,
       "legend": {
@@ -2323,7 +2513,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 75
+        "y": 83
       },
       "id": 26,
       "legend": {
@@ -2410,7 +2600,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 83
+        "y": 91
       },
       "id": 23,
       "legend": {
@@ -2498,7 +2688,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 83
+        "y": 91
       },
       "id": 24,
       "legend": {
@@ -2708,5 +2898,5 @@
   "timezone": "",
   "title": "Datasource tests - MSSQL (unit test)",
   "uid": "GlAqcPgmz",
-  "version": 10
+  "version": 2
 }
\ No newline at end of file
diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
index cec8ebe9d02..0255f3c0c91 100644
--- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1533714324007,
+  "iteration": 1534508678095,
   "links": [],
   "panels": [
     {
@@ -1191,6 +1191,190 @@
         "x": 0,
         "y": 27
       },
+      "id": 38,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(timeInt32, '$summarize'), \n  measurement, \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(timeInt32) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1, 2",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mysql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 27
+      },
+      "id": 39,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "MovingAverageValueOne",
+          "dashes": true,
+          "lines": false
+        },
+        {
+          "alias": "MovingAverageValueTwo",
+          "dashes": true,
+          "lines": false,
+          "yaxis": 1
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(timeInt32, '$summarize'), \n  avg(valueOne) as valueOne,\n  avg(valueTwo) as valueTwo\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(timeInt32) AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-mysql-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 35
+      },
       "id": 4,
       "legend": {
         "alignAsTable": true,
@@ -1276,7 +1460,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 27
+        "y": 35
       },
       "id": 28,
       "legend": {
@@ -1361,7 +1545,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 35
+        "y": 43
       },
       "id": 19,
       "legend": {
@@ -1448,7 +1632,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 35
+        "y": 43
       },
       "id": 18,
       "legend": {
@@ -1533,7 +1717,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 43
+        "y": 51
       },
       "id": 17,
       "legend": {
@@ -1620,7 +1804,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 43
+        "y": 51
       },
       "id": 20,
       "legend": {
@@ -1705,7 +1889,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 51
+        "y": 59
       },
       "id": 14,
       "legend": {
@@ -1793,7 +1977,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 51
+        "y": 59
       },
       "id": 15,
       "legend": {
@@ -1880,7 +2064,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 59
+        "y": 67
       },
       "id": 25,
       "legend": {
@@ -1968,7 +2152,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 59
+        "y": 67
       },
       "id": 22,
       "legend": {
@@ -2055,7 +2239,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 67
+        "y": 75
       },
       "id": 21,
       "legend": {
@@ -2143,7 +2327,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 67
+        "y": 75
       },
       "id": 26,
       "legend": {
@@ -2230,7 +2414,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 75
+        "y": 83
       },
       "id": 23,
       "legend": {
@@ -2318,7 +2502,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 75
+        "y": 83
       },
       "id": 24,
       "legend": {
@@ -2526,5 +2710,5 @@
   "timezone": "",
   "title": "Datasource tests - MySQL (unittest)",
   "uid": "Hmf8FDkmz",
-  "version": 9
+  "version": 2
 }
\ No newline at end of file
diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
index cc93308e116..3c56868e9ff 100644
--- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
+++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json
@@ -64,7 +64,7 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1533714184500,
+  "iteration": 1534507993194,
   "links": [],
   "panels": [
     {
@@ -1179,6 +1179,178 @@
         "x": 0,
         "y": 27
       },
+      "id": 38,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(\"timeInt32\", '$summarize'), \n  measurement, \n  avg(\"valueOne\") as \"valueOne\",\n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(\"timeInt32\") AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1, 2",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-postgres-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 27
+      },
+      "id": 39,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__unixEpochGroupAlias(\"timeInt32\", '$summarize'), \n  avg(\"valueOne\") as \"valueOne\",\n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__unixEpochFilter(\"timeInt32\") AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column using timeGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-postgres-ds-tests",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 35
+      },
       "id": 4,
       "legend": {
         "alignAsTable": true,
@@ -1264,7 +1436,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 27
+        "y": 35
       },
       "id": 28,
       "legend": {
@@ -1349,7 +1521,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 35
+        "y": 43
       },
       "id": 19,
       "legend": {
@@ -1436,7 +1608,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 35
+        "y": 43
       },
       "id": 18,
       "legend": {
@@ -1521,7 +1693,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 43
+        "y": 51
       },
       "id": 17,
       "legend": {
@@ -1608,7 +1780,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 43
+        "y": 51
       },
       "id": 20,
       "legend": {
@@ -1693,7 +1865,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 51
+        "y": 59
       },
       "id": 14,
       "legend": {
@@ -1781,7 +1953,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 51
+        "y": 59
       },
       "id": 15,
       "legend": {
@@ -1868,7 +2040,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 59
+        "y": 67
       },
       "id": 25,
       "legend": {
@@ -1956,7 +2128,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 59
+        "y": 67
       },
       "id": 22,
       "legend": {
@@ -2043,7 +2215,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 67
+        "y": 75
       },
       "id": 21,
       "legend": {
@@ -2131,7 +2303,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 67
+        "y": 75
       },
       "id": 26,
       "legend": {
@@ -2218,7 +2390,7 @@
         "h": 8,
         "w": 12,
         "x": 0,
-        "y": 75
+        "y": 83
       },
       "id": 23,
       "legend": {
@@ -2306,7 +2478,7 @@
         "h": 8,
         "w": 12,
         "x": 12,
-        "y": 75
+        "y": 83
       },
       "id": 24,
       "legend": {
@@ -2518,5 +2690,5 @@
   "timezone": "",
   "title": "Datasource tests - Postgres (unittest)",
   "uid": "vHQdlVziz",
-  "version": 9
+  "version": 1
 }
\ No newline at end of file

From eaa169cc476ee646f83bc792f3979a2c6504daf6 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 17 Aug 2018 14:40:50 +0200
Subject: [PATCH 360/380] docs: es versions supported

---
 docs/sources/features/datasources/elasticsearch.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md
index d29327cf480..80a2f9a828a 100644
--- a/docs/sources/features/datasources/elasticsearch.md
+++ b/docs/sources/features/datasources/elasticsearch.md
@@ -58,8 +58,8 @@ a time pattern for the index name or a wildcard.
 
 ### Elasticsearch version
 
-Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. Currently only 2.x and 5.x
-are supported.
+Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed.
+Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example.
 
 ### Min time interval
 A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.

From e3b585d8623b8fb7f78a4f2b9663f8a20fbf7467 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 17 Aug 2018 14:44:29 +0200
Subject: [PATCH 361/380] changelog: add notes about closing #12892

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34d3c82916e..ee52016ee70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
 * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
+* **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)

From 0a7be2618e23822c987468d3d506476597744db8 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 17 Aug 2018 15:00:37 +0200
Subject: [PATCH 362/380] cleaning up test data

---
 pkg/services/provisioning/datasources/config_reader_test.go     | 2 +-
 .../datasources/testdata/multiple-org-default/config.yaml       | 2 --
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go
index df99456b21b..07c8d68e75c 100644
--- a/pkg/services/provisioning/datasources/config_reader_test.go
+++ b/pkg/services/provisioning/datasources/config_reader_test.go
@@ -74,7 +74,7 @@ func TestDatasourceAsConfig(t *testing.T) {
 			})
 		})
 
-		Convey("Multiple datasources in different organizations with is_default in each organization", func() {
+		Convey("Multiple datasources in different organizations with isDefault in each organization", func() {
 			dc := newDatasourceProvisioner(logger)
 			err := dc.applyChanges(multipleOrgsWithDefault)
 			Convey("should not raise error", func() {
diff --git a/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml b/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
index 447317a8c77..f185abb6f53 100644
--- a/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
+++ b/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml
@@ -11,7 +11,6 @@ datasources:
     type: graphite
     access: proxy
     url: http://localhost:8080
-    is_default: true
   - orgId: 2
     name: prometheus
     type: prometheus
@@ -23,5 +22,4 @@ datasources:
     type: graphite
     access: proxy
     url: http://localhost:8080
-    is_default: true
 

From bcd876f88e25f93582c67bbc447907b7683e1f48 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 17 Aug 2018 15:40:44 +0200
Subject: [PATCH 363/380] changelog: add notes about closing #12229

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee52016ee70..98efa2b1099 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,6 +52,7 @@ om/grafana/grafana/issues/12668)
 * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
 * **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
 * **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
+* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229)
 
 ### Breaking changes
 

From d58986872c52ee6349f70e33d0d0f5c4947152a2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Francisco=20Guimar=C3=A3es?= <francisco.cpg@gmail.com>
Date: Fri, 17 Aug 2018 11:04:32 -0300
Subject: [PATCH 364/380] Replacing variable interpolation in "All value" value

---
 public/app/features/templating/template_srv.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts
index fc79d12ff9e..5bbf5effa66 100644
--- a/public/app/features/templating/template_srv.ts
+++ b/public/app/features/templating/template_srv.ts
@@ -209,7 +209,7 @@ export class TemplateSrv {
         value = this.getAllValue(variable);
         // skip formatting of custom all values
         if (variable.allValue) {
-          return value;
+          return this.replace(value);
         }
       }
 

From da2822c88ddcf51fb457ffc6b7f646bb2e22b73d Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Fri, 17 Aug 2018 14:51:01 +0200
Subject: [PATCH 365/380] alerting: inline docs for the slack channel.

Related to #12944
---
 pkg/services/alerting/notifiers/slack.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go
index a8139b62726..c1dadba414d 100644
--- a/pkg/services/alerting/notifiers/slack.go
+++ b/pkg/services/alerting/notifiers/slack.go
@@ -58,7 +58,7 @@ func init() {
           data-placement="right">
         </input>
         <info-popover mode="right-absolute">
-          Provide a bot token to use the Slack file.upload API (starts with "xoxb")
+          Provide a bot token to use the Slack file.upload API (starts with "xoxb"). Specify #channel-name or @username in Recipient for this to work 
         </info-popover>
       </div>
     `,

From c75e07121381a31cc0504c5a69bbaa468ac212ab Mon Sep 17 00:00:00 2001
From: Daniel Lee <dan.limerick@gmail.com>
Date: Sat, 18 Aug 2018 16:00:40 +0200
Subject: [PATCH 366/380] dsproxy: interpolate route url

Allows for dynamic urls for plugin routes. There are a few plugins
where the route url should be configurable and this change allows
using jsonData fields in the url field for a route in the
plugin.json file for a plugin.
---
 pkg/api/pluginproxy/ds_proxy.go      | 10 ++++++++--
 pkg/api/pluginproxy/ds_proxy_test.go | 21 ++++++++++++++++++++-
 2 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go
index c8056040d24..fb2cab9b9b1 100644
--- a/pkg/api/pluginproxy/ds_proxy.go
+++ b/pkg/api/pluginproxy/ds_proxy.go
@@ -320,9 +320,15 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
 		SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
 	}
 
-	routeURL, err := url.Parse(proxy.route.Url)
+	interpolatedURL, err := interpolateString(proxy.route.Url, data)
 	if err != nil {
-		logger.Error("Error parsing plugin route url")
+		logger.Error("Error interpolating proxy url", "error", err)
+		return
+	}
+
+	routeURL, err := url.Parse(interpolatedURL)
+	if err != nil {
+		logger.Error("Error parsing plugin route url", "error", err)
 		return
 	}
 
diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go
index ad331113f46..e6d05872787 100644
--- a/pkg/api/pluginproxy/ds_proxy_test.go
+++ b/pkg/api/pluginproxy/ds_proxy_test.go
@@ -49,6 +49,13 @@ func TestDSRouteRule(t *testing.T) {
 							{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
 						},
 					},
+					{
+						Path: "api/common",
+						Url:  "{{.JsonData.dynamicUrl}}",
+						Headers: []plugins.AppPluginRouteHeader{
+							{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
+						},
+					},
 				},
 			}
 
@@ -57,7 +64,8 @@ func TestDSRouteRule(t *testing.T) {
 
 			ds := &m.DataSource{
 				JsonData: simplejson.NewFromAny(map[string]interface{}{
-					"clientId": "asd",
+					"clientId":   "asd",
+					"dynamicUrl": "https://dynamic.grafana.com",
 				}),
 				SecureJsonData: map[string][]byte{
 					"key": key,
@@ -83,6 +91,17 @@ func TestDSRouteRule(t *testing.T) {
 				})
 			})
 
+			Convey("When matching route path and has dynamic url", func() {
+				proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method")
+				proxy.route = plugin.Routes[3]
+				proxy.applyRoute(req)
+
+				Convey("should add headers and interpolate the url", func() {
+					So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method")
+					So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
+				})
+			})
+
 			Convey("Validating request", func() {
 				Convey("plugin route with valid role", func() {
 					proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")

From a92d51731d36b818713b0d0004be8f598ba962af Mon Sep 17 00:00:00 2001
From: Pierre GIRAUD <pierre.giraud@gmail.com>
Date: Mon, 20 Aug 2018 11:55:29 +0200
Subject: [PATCH 367/380] Webpack tapable plugin deprecation (#12960)

* Remove unrequired extract-text-webpack-plugin

* Update ngAnnotate to avoid deprecation warning

* Avoid deprecation warning (Tapable.plugin -> hooks)
---
 package.json                    |  3 +--
 scripts/webpack/webpack.dev.js  |  1 -
 scripts/webpack/webpack.prod.js |  2 +-
 yarn.lock                       | 15 +++------------
 4 files changed, 5 insertions(+), 16 deletions(-)

diff --git a/package.json b/package.json
index 87615e8273b..8520def5db8 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,6 @@
     "es6-shim": "^0.35.3",
     "expect.js": "~0.2.0",
     "expose-loader": "^0.7.3",
-    "extract-text-webpack-plugin": "^4.0.0-beta.0",
     "file-loader": "^1.1.11",
     "fork-ts-checker-webpack-plugin": "^0.4.2",
     "gaze": "^1.1.2",
@@ -63,7 +62,7 @@
     "mobx-react-devtools": "^4.2.15",
     "mocha": "^4.0.1",
     "ng-annotate-loader": "^0.6.1",
-    "ng-annotate-webpack-plugin": "^0.2.1-pre",
+    "ng-annotate-webpack-plugin": "^0.3.0",
     "ngtemplate-loader": "^2.0.1",
     "npm": "^5.4.2",
     "optimize-css-assets-webpack-plugin": "^4.0.2",
diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js
index 1e54ca73a19..7eecceeb1bf 100644
--- a/scripts/webpack/webpack.dev.js
+++ b/scripts/webpack/webpack.dev.js
@@ -5,7 +5,6 @@ const common = require('./webpack.common.js');
 const path = require('path');
 const webpack = require('webpack');
 const HtmlWebpackPlugin = require("html-webpack-plugin");
-const ExtractTextPlugin = require("extract-text-webpack-plugin");
 const CleanWebpackPlugin = require('clean-webpack-plugin');
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
diff --git a/scripts/webpack/webpack.prod.js b/scripts/webpack/webpack.prod.js
index 9c0b0cd093c..9e1e4cfb0b5 100644
--- a/scripts/webpack/webpack.prod.js
+++ b/scripts/webpack/webpack.prod.js
@@ -81,7 +81,7 @@ module.exports = merge(common, {
       chunks: ['vendor', 'app'],
     }),
     function () {
-      this.plugin("done", function (stats) {
+      this.hooks.done.tap('Done', function (stats) {
         if (stats.compilation.errors && stats.compilation.errors.length) {
           console.log(stats.compilation.errors);
           process.exit(1);
diff --git a/yarn.lock b/yarn.lock
index c4bd6704839..dd1cde4e698 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4105,15 +4105,6 @@ extglob@^2.0.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
-extract-text-webpack-plugin@^4.0.0-beta.0:
-  version "4.0.0-beta.0"
-  resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-4.0.0-beta.0.tgz#f7361d7ff430b42961f8d1321ba8c1757b5d4c42"
-  dependencies:
-    async "^2.4.1"
-    loader-utils "^1.1.0"
-    schema-utils "^0.4.5"
-    webpack-sources "^1.1.0"
-
 extract-zip@^1.6.5:
   version "1.6.7"
   resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
@@ -7661,9 +7652,9 @@ ng-annotate-loader@^0.6.1:
     normalize-path "2.0.1"
     source-map "0.5.6"
 
-ng-annotate-webpack-plugin@^0.2.1-pre:
-  version "0.2.1-pre"
-  resolved "https://registry.yarnpkg.com/ng-annotate-webpack-plugin/-/ng-annotate-webpack-plugin-0.2.1-pre.tgz#40d9aa8cd214e30e3125a8481634ab0dd9b3dd68"
+ng-annotate-webpack-plugin@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/ng-annotate-webpack-plugin/-/ng-annotate-webpack-plugin-0.3.0.tgz#2e7f5e29c6a4ce26649edcb06c1213408b35b84a"
   dependencies:
     ng-annotate "^1.2.1"
     webpack-core "^0.6.5"

From 0223a75de00f23c45fe4140b995f285570d62671 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Francisco=20Guimar=C3=A3es?= <francisco.cpg@gmail.com>
Date: Mon, 20 Aug 2018 06:56:12 -0300
Subject: [PATCH 368/380] Refresh query variable when another variable is used
 in regex field (#12961)

---
 public/app/features/templating/query_variable.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts
index 5ddd6d32864..827fd80a176 100644
--- a/public/app/features/templating/query_variable.ts
+++ b/public/app/features/templating/query_variable.ts
@@ -213,7 +213,7 @@ export class QueryVariable implements Variable {
   }
 
   dependsOn(variable) {
-    return containsVariable(this.query, this.datasource, variable.name);
+    return containsVariable(this.query, this.datasource, this.regex, variable.name);
   }
 }
 

From cf632c0f11b8b3a18abb4c2e8eacb0b79794728f Mon Sep 17 00:00:00 2001
From: Pierre GIRAUD <pierre.giraud@gmail.com>
Date: Mon, 20 Aug 2018 19:21:32 +0200
Subject: [PATCH 369/380] Fix bulk-dashboards path (#12978)

---
 .gitignore                                  | 2 +-
 devenv/bulk-dashboards/bulk-dashboards.yaml | 2 +-
 devenv/setup.sh                             | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.gitignore b/.gitignore
index 2484176a469..bf97948d178 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,4 +71,4 @@ debug.test
 /vendor/**/appengine*
 *.orig
 
-/devenv/dashboards/bulk-testing/*.json
+/devenv/bulk-dashboards/*.json
diff --git a/devenv/bulk-dashboards/bulk-dashboards.yaml b/devenv/bulk-dashboards/bulk-dashboards.yaml
index e0ba8a88e68..65557901f42 100644
--- a/devenv/bulk-dashboards/bulk-dashboards.yaml
+++ b/devenv/bulk-dashboards/bulk-dashboards.yaml
@@ -5,5 +5,5 @@ providers:
    folder: 'Bulk dashboards'
    type: file
    options:
-     path: devenv/dashboards/bulk-testing
+     path: devenv/bulk-dashboards
 
diff --git a/devenv/setup.sh b/devenv/setup.sh
index 6412bbc98ea..cc71ecc71bf 100755
--- a/devenv/setup.sh
+++ b/devenv/setup.sh
@@ -7,11 +7,11 @@ bulkDashboard() {
 		COUNTER=0
 		MAX=400
 		while [  $COUNTER -lt $MAX ]; do
-				jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + {  uid: 'uid-${COUNTER}',  title: 'title-${COUNTER}' }"
+				jsonnet -o "bulk-dashboards/dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk-dashboards/bulkdash.jsonnet'; bulkDash + {  uid: 'uid-${COUNTER}',  title: 'title-${COUNTER}' }"
 				let COUNTER=COUNTER+1
 		done
 
-		ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
+		ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
 }
 
 requiresJsonnet() {

From 72efd73c3baaf7d8721309ad9002dc749d81a306 Mon Sep 17 00:00:00 2001
From: Pierre GIRAUD <pierre.giraud@gmail.com>
Date: Mon, 20 Aug 2018 19:22:30 +0200
Subject: [PATCH 370/380] Show min-width option only for horizontal repeat
 (#12981)

---
 public/app/partials/panelgeneral.html | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/public/app/partials/panelgeneral.html b/public/app/partials/panelgeneral.html
index eb17d743d33..797c252331c 100644
--- a/public/app/partials/panelgeneral.html
+++ b/public/app/partials/panelgeneral.html
@@ -18,18 +18,18 @@
 			<span class="gf-form-label width-9">For each value of</span>
 			<dash-repeat-option panel="ctrl.panel"></dash-repeat-option>
 		</div>
-		<div class="gf-form" ng-show="ctrl.panel.repeat">
-			<span class="gf-form-label width-9">Min width</span>
-			<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]">
-				<option value=""></option>
-			</select>
-		</div>
     <div class="gf-form" ng-show="ctrl.panel.repeat">
 			<span class="gf-form-label width-9">Direction</span>
 			<select class="gf-form-input" ng-model="ctrl.panel.repeatDirection" ng-options="f.value as f.text for f in [{value: 'v', text: 'Vertical'}, {value: 'h', text: 'Horizontal'}]">
 				<option value=""></option>
 			</select>
 		</div>
+		<div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
+			<span class="gf-form-label width-9">Min width</span>
+			<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]">
+				<option value=""></option>
+			</select>
+		</div>
 	</div>
 
 	<panel-links-editor panel="ctrl.panel"></panel-links-editor>

From 6316d637f1c68fcad3f9ccdf62f6b7d8371fffc8 Mon Sep 17 00:00:00 2001
From: David <david.kaltschmidt@gmail.com>
Date: Mon, 20 Aug 2018 19:22:55 +0200
Subject: [PATCH 371/380] Explore: Apply tab completion suggestion on Enter
 (#12904)

- if the suggestions menu is open, apply the selected item on Enter
- if not open, run the queries
---
 public/app/containers/Explore/QueryField.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx
index 04481885a1c..52bfbc7fed4 100644
--- a/public/app/containers/Explore/QueryField.tsx
+++ b/public/app/containers/Explore/QueryField.tsx
@@ -331,7 +331,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
         }
         break;
       }
-
+      case 'Enter':
       case 'Tab': {
         if (this.menuEl) {
           // Dont blur input

From eef0d280825781937d381e7e41a17d087055af28 Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Tue, 21 Aug 2018 10:51:38 +0200
Subject: [PATCH 372/380] changelog: add notes about closing #11890

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98efa2b1099..651b4dd22f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
 * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
+* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890)
 * **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)

From 92ed1f04afcdead02fe3a8bf53caecc89db1c5dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?= <torkel@grafana.com>
Date: Tue, 21 Aug 2018 13:30:39 +0200
Subject: [PATCH 373/380] sql: added code migration type

---
 pkg/api/login.go                             |  8 +++-
 pkg/services/sqlstore/migrations/user_mig.go | 41 +++++++++++++++++++-
 pkg/services/sqlstore/migrator/migrator.go   | 16 +++++---
 pkg/services/sqlstore/migrator/types.go      |  7 ++++
 pkg/services/sqlstore/user.go                |  5 ++-
 pkg/services/sqlstore/user_test.go           | 22 +++++++++++
 6 files changed, 90 insertions(+), 9 deletions(-)

diff --git a/pkg/api/login.go b/pkg/api/login.go
index 01fa71a6e44..632d04e37f1 100644
--- a/pkg/api/login.go
+++ b/pkg/api/login.go
@@ -78,7 +78,13 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
 	user := userQuery.Result
 
 	// validate remember me cookie
-	if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login {
+	signingKey := user.Rands + user.Password
+	if len(signingKey) < 10 {
+		c.Logger.Error("Invalid user signingKey")
+		return false
+	}
+
+	if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
 		return false
 	}
 
diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go
index edcfbb7b889..400033aaa33 100644
--- a/pkg/services/sqlstore/migrations/user_mig.go
+++ b/pkg/services/sqlstore/migrations/user_mig.go
@@ -1,6 +1,12 @@
 package migrations
 
-import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+import (
+	"fmt"
+
+	"github.com/go-xorm/xorm"
+	. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+	"github.com/grafana/grafana/pkg/util"
+)
 
 func addUserMigrations(mg *Migrator) {
 	userV1 := Table{
@@ -107,4 +113,37 @@ func addUserMigrations(mg *Migrator) {
 	mg.AddMigration("Add last_seen_at column to user", NewAddColumnMigration(userV2, &Column{
 		Name: "last_seen_at", Type: DB_DateTime, Nullable: true,
 	}))
+
+	// Adds salt & rands for old users who used ldap or oauth
+	mg.AddMigration("Add missing user data", &AddMissingUserSaltAndRandsMigration{})
+}
+
+type AddMissingUserSaltAndRandsMigration struct {
+	MigrationBase
+}
+
+func (m *AddMissingUserSaltAndRandsMigration) Sql(dialect Dialect) string {
+	return "code migration"
+}
+
+type TempUserDTO struct {
+	Id    int64
+	Login string
+}
+
+func (m *AddMissingUserSaltAndRandsMigration) Exec(sess *xorm.Session, mg *Migrator) error {
+	users := make([]*TempUserDTO, 0)
+
+	err := sess.Sql(fmt.Sprintf("SELECT id, login from %s WHERE rands = ''", mg.Dialect.Quote("user"))).Find(&users)
+	if err != nil {
+		return err
+	}
+
+	for _, user := range users {
+		_, err := sess.Exec("UPDATE "+mg.Dialect.Quote("user")+" SET salt = ?, rands = ? WHERE id = ?", util.GetRandomString(10), util.GetRandomString(10), user.Id)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go
index 9bdaaf7cc14..dead6f2b416 100644
--- a/pkg/services/sqlstore/migrator/migrator.go
+++ b/pkg/services/sqlstore/migrator/migrator.go
@@ -12,7 +12,7 @@ import (
 
 type Migrator struct {
 	x          *xorm.Engine
-	dialect    Dialect
+	Dialect    Dialect
 	migrations []Migration
 	Logger     log.Logger
 }
@@ -31,7 +31,7 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
 	mg.x = engine
 	mg.Logger = log.New("migrator")
 	mg.migrations = make([]Migration, 0)
-	mg.dialect = NewDialect(mg.x)
+	mg.Dialect = NewDialect(mg.x)
 	return mg
 }
 
@@ -86,7 +86,7 @@ func (mg *Migrator) Start() error {
 			continue
 		}
 
-		sql := m.Sql(mg.dialect)
+		sql := m.Sql(mg.Dialect)
 
 		record := MigrationLog{
 			MigrationId: m.Id(),
@@ -122,7 +122,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
 
 	condition := m.GetCondition()
 	if condition != nil {
-		sql, args := condition.Sql(mg.dialect)
+		sql, args := condition.Sql(mg.Dialect)
 		results, err := sess.SQL(sql).Query(args...)
 		if err != nil || len(results) == 0 {
 			mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id())
@@ -130,7 +130,13 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
 		}
 	}
 
-	_, err := sess.Exec(m.Sql(mg.dialect))
+	var err error
+	if codeMigration, ok := m.(CodeMigration); ok {
+		err = codeMigration.Exec(sess, mg)
+	} else {
+		_, err = sess.Exec(m.Sql(mg.Dialect))
+	}
+
 	if err != nil {
 		mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
 		return err
diff --git a/pkg/services/sqlstore/migrator/types.go b/pkg/services/sqlstore/migrator/types.go
index 26c46889daf..48354998d8d 100644
--- a/pkg/services/sqlstore/migrator/types.go
+++ b/pkg/services/sqlstore/migrator/types.go
@@ -3,6 +3,8 @@ package migrator
 import (
 	"fmt"
 	"strings"
+
+	"github.com/go-xorm/xorm"
 )
 
 const (
@@ -19,6 +21,11 @@ type Migration interface {
 	GetCondition() MigrationCondition
 }
 
+type CodeMigration interface {
+	Migration
+	Exec(sess *xorm.Session, migrator *Migrator) error
+}
+
 type SQLType string
 
 type ColumnType string
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index 0ec1a947870..5d1b827e79f 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -113,9 +113,10 @@ func CreateUser(ctx context.Context, cmd *m.CreateUserCommand) error {
 			LastSeenAt:    time.Now().AddDate(-10, 0, 0),
 		}
 
+		user.Salt = util.GetRandomString(10)
+		user.Rands = util.GetRandomString(10)
+
 		if len(cmd.Password) > 0 {
-			user.Salt = util.GetRandomString(10)
-			user.Rands = util.GetRandomString(10)
 			user.Password = util.EncodePassword(cmd.Password, user.Salt)
 		}
 
diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go
index a76ae860b7d..b26dd235772 100644
--- a/pkg/services/sqlstore/user_test.go
+++ b/pkg/services/sqlstore/user_test.go
@@ -15,6 +15,28 @@ func TestUserDataAccess(t *testing.T) {
 	Convey("Testing DB", t, func() {
 		InitTestDB(t)
 
+		Convey("Creating a user", func() {
+			cmd := &m.CreateUserCommand{
+				Email: "usertest@test.com",
+				Name:  "user name",
+				Login: "user_test_login",
+			}
+
+			err := CreateUser(context.Background(), cmd)
+			So(err, ShouldBeNil)
+
+			Convey("Loading a user", func() {
+				query := m.GetUserByIdQuery{Id: cmd.Result.Id}
+				err := GetUserById(&query)
+				So(err, ShouldBeNil)
+
+				So(query.Result.Email, ShouldEqual, "usertest@test.com")
+				So(query.Result.Password, ShouldEqual, "")
+				So(query.Result.Rands, ShouldHaveLength, 10)
+				So(query.Result.Salt, ShouldHaveLength, 10)
+			})
+		})
+
 		Convey("Given 5 users", func() {
 			var err error
 			var cmd *m.CreateUserCommand

From 2b1f84cd43c0d9175d7c9bd353629f77d39b7ad3 Mon Sep 17 00:00:00 2001
From: Stefan <stefanes@users.noreply.github.com>
Date: Tue, 21 Aug 2018 15:53:57 +0200
Subject: [PATCH 374/380] Update notifications.md

---
 docs/sources/alerting/notifications.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md
index b3b4305a748..58046cafae4 100644
--- a/docs/sources/alerting/notifications.md
+++ b/docs/sources/alerting/notifications.md
@@ -130,7 +130,7 @@ There are a couple of configuration options which need to be set up in Grafana U
 
 Once these two properties are set, you can send the alerts to Kafka for further processing or throttling.
 
-### All supported notifier
+### All supported notifiers
 
 Name | Type |Support images
 -----|------------ | ------
@@ -148,6 +148,7 @@ Pushover | `pushover` | no
 Telegram | `telegram` | no
 Line | `line` | no
 Prometheus Alertmanager | `prometheus-alertmanager` | no
+Microsoft Teams | `teams` | yes
 
 
 

From 8dc16755740df0fab2385cd123db21fbf4dd68ae Mon Sep 17 00:00:00 2001
From: Alexander Zobnin <alexanderzobnin@gmail.com>
Date: Wed, 22 Aug 2018 15:29:09 +0300
Subject: [PATCH 375/380] heatmap: fix tooltip bug in firefox

---
 .../plugins/panel/heatmap/heatmap_tooltip.ts  | 28 +++++----------
 public/app/plugins/panel/heatmap/rendering.ts | 35 +++++++++++++------
 2 files changed, 34 insertions(+), 29 deletions(-)

diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
index 1caa9ae9c69..6cf9262f520 100644
--- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
+++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
@@ -28,21 +28,9 @@ export class HeatmapTooltip {
     this.mouseOverBucket = false;
     this.originalFillColor = null;
 
-    elem.on('mouseover', this.onMouseOver.bind(this));
     elem.on('mouseleave', this.onMouseLeave.bind(this));
   }
 
-  onMouseOver(e) {
-    if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data.buckets)) {
-      return;
-    }
-
-    if (!this.tooltip) {
-      this.add();
-      this.move(e);
-    }
-  }
-
   onMouseLeave() {
     this.destroy();
   }
@@ -81,11 +69,15 @@ export class HeatmapTooltip {
 
     let { xBucketIndex, yBucketIndex } = this.getBucketIndexes(pos, data);
 
-    if (!data.buckets[xBucketIndex] || !this.tooltip) {
+    if (!data.buckets[xBucketIndex]) {
       this.destroy();
       return;
     }
 
+    if (!this.tooltip) {
+      this.add();
+    }
+
     let boundBottom, boundTop, valuesNumber;
     let xData = data.buckets[xBucketIndex];
     // Search in special 'zero' bucket also
@@ -158,13 +150,12 @@ export class HeatmapTooltip {
   }
 
   getBucketIndexes(pos, data) {
-    const xBucketIndex = this.getXBucketIndex(pos.offsetX, data);
-    const yBucketIndex = this.getYBucketIndex(pos.offsetY, data);
+    const xBucketIndex = this.getXBucketIndex(pos.x, data);
+    const yBucketIndex = this.getYBucketIndex(pos.y, data);
     return { xBucketIndex, yBucketIndex };
   }
 
-  getXBucketIndex(offsetX, data) {
-    let x = this.scope.xScale.invert(offsetX - this.scope.yAxisWidth).valueOf();
+  getXBucketIndex(x, data) {
     // First try to find X bucket by checking x pos is in the
     // [bucket.x, bucket.x + xBucketSize] interval
     let xBucket = _.find(data.buckets, bucket => {
@@ -173,8 +164,7 @@ export class HeatmapTooltip {
     return xBucket ? xBucket.x : getValueBucketBound(x, data.xBucketSize, 1);
   }
 
-  getYBucketIndex(offsetY, data) {
-    let y = this.scope.yScale.invert(offsetY - this.scope.chartTop);
+  getYBucketIndex(y, data) {
     if (data.tsBuckets) {
       return Math.floor(y);
     }
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index e3318ea7e23..8ea216be89d 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/public/app/plugins/panel/heatmap/rendering.ts
@@ -69,6 +69,7 @@ export class HeatmapRenderer {
     this.ctrl.events.on('render', this.onRender.bind(this));
 
     this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this);
+
     /////////////////////////////
     // Selection and crosshair //
     /////////////////////////////
@@ -678,9 +679,17 @@ export class HeatmapRenderer {
     }
   }
 
+  getEventOffset(event) {
+    const elemOffset = this.$heatmap.offset();
+    const x = Math.floor(event.clientX - elemOffset.left);
+    const y = Math.floor(event.clientY - elemOffset.top);
+    return { x, y };
+  }
+
   onMouseDown(event) {
+    const offset = this.getEventOffset(event);
     this.selection.active = true;
-    this.selection.x1 = event.offsetX;
+    this.selection.x1 = offset.x;
 
     this.mouseUpHandler = () => {
       this.onMouseUp();
@@ -718,23 +727,25 @@ export class HeatmapRenderer {
       return;
     }
 
+    const offset = this.getEventOffset(event);
     if (this.selection.active) {
       // Clear crosshair and tooltip
       this.clearCrosshair();
       this.tooltip.destroy();
 
-      this.selection.x2 = this.limitSelection(event.offsetX);
+      this.selection.x2 = this.limitSelection(offset.x);
       this.drawSelection(this.selection.x1, this.selection.x2);
     } else {
-      this.emitGraphHoverEvent(event);
-      this.drawCrosshair(event.offsetX);
-      this.tooltip.show(event, this.data);
+      const pos = this.getEventPos(event, offset);
+      this.drawCrosshair(offset.x);
+      this.tooltip.show(pos, this.data);
+      this.emitGraphHoverEvent(pos);
     }
   }
 
-  emitGraphHoverEvent(event) {
-    let x = this.xScale.invert(event.offsetX - this.yAxisWidth).valueOf();
-    let y = this.yScale.invert(event.offsetY);
+  getEventPos(event, offset) {
+    let x = this.xScale.invert(offset.x - this.yAxisWidth).valueOf();
+    let y = this.yScale.invert(offset.y - this.chartTop);
     let pos = {
       pageX: event.pageX,
       pageY: event.pageY,
@@ -743,11 +754,15 @@ export class HeatmapRenderer {
       y: y,
       y1: y,
       panelRelY: null,
+      offset,
     };
 
-    // Set minimum offset to prevent showing legend from another panel
-    pos.panelRelY = Math.max(event.offsetY / this.height, 0.001);
+    return pos;
+  }
 
+  emitGraphHoverEvent(pos) {
+    // Set minimum offset to prevent showing legend from another panel
+    pos.panelRelY = Math.max(pos.offset.y / this.height, 0.001);
     // broadcast to other graph panels that we are hovering
     appEvents.emit('graph-hover', { pos: pos, panel: this.panel });
   }

From 060fb1af05fdf13852fb619f2ec41dda442552cd Mon Sep 17 00:00:00 2001
From: Alexander Zobnin <alexanderzobnin@gmail.com>
Date: Thu, 23 Aug 2018 13:00:15 +0300
Subject: [PATCH 376/380] tests: fix missing tests (with .jest suffix)

---
 .../specs/{share_modal_ctrl.jest.ts => share_modal_ctrl.test.ts}  | 0
 .../influxdb/specs/{datasource.jest.ts => datasource.test.ts}     | 0
 2 files changed, 0 insertions(+), 0 deletions(-)
 rename public/app/features/dashboard/specs/{share_modal_ctrl.jest.ts => share_modal_ctrl.test.ts} (100%)
 rename public/app/plugins/datasource/influxdb/specs/{datasource.jest.ts => datasource.test.ts} (100%)

diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.jest.ts b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts
similarity index 100%
rename from public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
rename to public/app/features/dashboard/specs/share_modal_ctrl.test.ts
diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.jest.ts b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts
similarity index 100%
rename from public/app/plugins/datasource/influxdb/specs/datasource.jest.ts
rename to public/app/plugins/datasource/influxdb/specs/datasource.test.ts

From a9497f0a96278b64c2e7e1d888064238a2b64e0f Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Thu, 23 Aug 2018 18:30:28 +0200
Subject: [PATCH 377/380] changelog: add notes about closing #12486

[skip ci]
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 651b4dd22f6..7946530027f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -54,6 +54,7 @@ om/grafana/grafana/issues/12668)
 * **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
 * **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
 * **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229)
+* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486)
 
 ### Breaking changes
 

From 5bb0d2660464fd0ad70348351b6f2809d2350638 Mon Sep 17 00:00:00 2001
From: Leonard Gram <leo@xlson.com>
Date: Tue, 21 Aug 2018 09:33:48 +0200
Subject: [PATCH 378/380] build: fixes rpm build when using defaults.

Closes #12980
---
 build.go | 33 +++++++++++++++------------------
 1 file changed, 15 insertions(+), 18 deletions(-)

diff --git a/build.go b/build.go
index bcb9b2ddf7d..561dd70df0e 100644
--- a/build.go
+++ b/build.go
@@ -64,6 +64,10 @@ func main() {
 
 	readVersionFromPackageJson()
 
+	if pkgArch == "" {
+		pkgArch = goarch
+	}
+
 	log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
 
 	if flag.NArg() == 0 {
@@ -105,10 +109,17 @@ func main() {
 
 		case "package":
 			grunt(gruntBuildArg("build")...)
-			packageGrafana()
+			grunt(gruntBuildArg("package")...)
+			if goos == "linux" {
+				createLinuxPackages()
+			}
 
 		case "package-only":
-			packageGrafana()
+			grunt(gruntBuildArg("package")...)
+			if goos == "linux" {
+				createLinuxPackages()
+			}
+
 
 		case "pkg-rpm":
 			grunt(gruntBuildArg("release")...)
@@ -133,22 +144,6 @@ func main() {
 	}
 }
 
-func packageGrafana() {
-	platformArg := fmt.Sprintf("--platform=%v", goos)
-	previousPkgArch := pkgArch
-	if pkgArch == "" {
-		pkgArch = goarch
-	}
-	postProcessArgs := gruntBuildArg("package")
-	postProcessArgs = append(postProcessArgs, platformArg)
-	grunt(postProcessArgs...)
-	pkgArch = previousPkgArch
-
-	if goos == "linux" {
-		createLinuxPackages()
-	}
-}
-
 func makeLatestDistCopies() {
 	files, err := ioutil.ReadDir("dist")
 	if err != nil {
@@ -404,6 +399,8 @@ func gruntBuildArg(task string) []string {
 	if phjsToRelease != "" {
 		args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
 	}
+	args = append(args, fmt.Sprintf("--platform=%v", goos))
+
 	return args
 }
 

From e906d4bdba8524fcc7dc759dac74d9cf7faf563c Mon Sep 17 00:00:00 2001
From: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Date: Fri, 24 Aug 2018 15:55:20 +0200
Subject: [PATCH 379/380] changelog: add notes about closing #12952 #12965

[skip ci]
---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7946530027f..3befbc40eb8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,8 @@
 * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890)
+* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)

From 8a99fa269d08252ed5708b89ce0a3e3086326d45 Mon Sep 17 00:00:00 2001
From: Tobias Skarhed <dehrax@users.noreply.github.com>
Date: Fri, 24 Aug 2018 16:48:47 +0200
Subject: [PATCH 380/380] WIP Update tslint (#12922)

* Interface linting rule

* fix: changed model names in store files so that the interface names do not conflict with the model names
---
 package.json                                  |  3 +-
 .../AlertRuleList/AlertRuleList.tsx           |  8 +--
 .../{IContainerProps.ts => ContainerProps.ts} |  4 +-
 public/app/containers/Explore/Explore.tsx     |  6 +-
 .../ManageDashboards/FolderPermissions.tsx    |  4 +-
 .../ManageDashboards/FolderSettings.tsx       |  4 +-
 .../containers/ServerStats/ServerStats.tsx    |  4 +-
 public/app/containers/Teams/TeamGroupSync.tsx |  8 +--
 public/app/containers/Teams/TeamList.tsx      |  6 +-
 public/app/containers/Teams/TeamMembers.tsx   | 10 ++--
 public/app/containers/Teams/TeamPages.tsx     |  4 +-
 public/app/containers/Teams/TeamSettings.tsx  |  4 +-
 .../components/EmptyListCTA/EmptyListCTA.tsx  | 57 ++++++++++---------
 .../core/components/PageHeader/PageHeader.tsx |  4 +-
 .../app/core/components/PasswordStrength.tsx  | 19 +++----
 .../DisabledPermissionsListItem.tsx           |  4 +-
 .../components/Permissions/Permissions.tsx    |  4 +-
 .../Permissions/PermissionsList.tsx           |  4 +-
 .../components/Picker/DescriptionOption.tsx   |  4 +-
 .../core/components/Picker/PickerOption.tsx   |  4 +-
 .../core/components/TagFilter/TagBadge.tsx    |  4 +-
 .../core/components/TagFilter/TagFilter.tsx   |  4 +-
 .../core/components/TagFilter/TagOption.tsx   |  4 +-
 .../core/components/TagFilter/TagValue.tsx    |  4 +-
 .../app/core/components/Tooltip/Popover.tsx   |  4 +-
 .../app/core/components/Tooltip/Tooltip.tsx   |  4 +-
 .../components/colorpicker/ColorPalette.tsx   |  8 +--
 .../components/colorpicker/ColorPicker.tsx    |  4 +-
 .../colorpicker/ColorPickerPopover.tsx        | 35 +++++++-----
 .../colorpicker/SeriesColorPicker.tsx         |  4 +-
 .../components/colorpicker/SpectrumPicker.tsx | 28 ++++-----
 .../stores/AlertListStore/AlertListStore.ts   | 10 ++--
 public/app/stores/NavStore/NavStore.ts        |  4 +-
 public/app/stores/RootStore/RootStore.ts      |  4 +-
 public/app/stores/TeamsStore/TeamsStore.ts    | 36 ++++++------
 public/app/stores/store.ts                    |  4 +-
 tslint.json                                   |  1 +
 yarn.lock                                     | 12 ++++
 38 files changed, 179 insertions(+), 160 deletions(-)
 rename public/app/containers/{IContainerProps.ts => ContainerProps.ts} (92%)

diff --git a/package.json b/package.json
index 8520def5db8..9cc47ff71b8 100644
--- a/package.json
+++ b/package.json
@@ -169,7 +169,8 @@
     "slate-react": "^0.12.4",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
-    "tinycolor2": "^1.4.1"
+    "tinycolor2": "^1.4.1",
+    "tslint-react": "^3.6.0"
   },
   "resolutions": {
     "caniuse-db": "1.0.30000772"
diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx
index b61c7fbaac3..3c2da77c2a7 100644
--- a/public/app/containers/AlertRuleList/AlertRuleList.tsx
+++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx
@@ -3,14 +3,14 @@ import { hot } from 'react-hot-loader';
 import classNames from 'classnames';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore';
+import { AlertRule } from 'app/stores/AlertListStore/AlertListStore';
 import appEvents from 'app/core/app_events';
-import IContainerProps from 'app/containers/IContainerProps';
+import ContainerProps from 'app/containers/ContainerProps';
 import Highlighter from 'react-highlight-words';
 
 @inject('view', 'nav', 'alertList')
 @observer
-export class AlertRuleList extends React.Component<IContainerProps, any> {
+export class AlertRuleList extends React.Component<ContainerProps, any> {
   stateFilters = [
     { text: 'All', value: 'all' },
     { text: 'OK', value: 'ok' },
@@ -109,7 +109,7 @@ function AlertStateFilterOption({ text, value }) {
 }
 
 export interface AlertRuleItemProps {
-  rule: IAlertRule;
+  rule: AlertRule;
   search: string;
 }
 
diff --git a/public/app/containers/IContainerProps.ts b/public/app/containers/ContainerProps.ts
similarity index 92%
rename from public/app/containers/IContainerProps.ts
rename to public/app/containers/ContainerProps.ts
index 6e790cee06d..97889278fdc 100644
--- a/public/app/containers/IContainerProps.ts
+++ b/public/app/containers/ContainerProps.ts
@@ -6,7 +6,7 @@ import { AlertListStore } from './../stores/AlertListStore/AlertListStore';
 import { ViewStore } from './../stores/ViewStore/ViewStore';
 import { FolderStore } from './../stores/FolderStore/FolderStore';
 
-interface IContainerProps {
+interface ContainerProps {
   search: typeof SearchStore.Type;
   serverStats: typeof ServerStatsStore.Type;
   nav: typeof NavStore.Type;
@@ -17,4 +17,4 @@ interface IContainerProps {
   backendSrv: any;
 }
 
-export default IContainerProps;
+export default ContainerProps;
diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx
index d161e7689cf..e06f4e7042e 100644
--- a/public/app/containers/Explore/Explore.tsx
+++ b/public/app/containers/Explore/Explore.tsx
@@ -63,7 +63,7 @@ function parseUrlState(initial: string | undefined) {
   return { datasource: null, queries: [], range: DEFAULT_RANGE };
 }
 
-interface IExploreState {
+interface ExploreState {
   datasource: any;
   datasourceError: any;
   datasourceLoading: boolean | null;
@@ -88,12 +88,12 @@ interface IExploreState {
   tableResult: any;
 }
 
-export class Explore extends React.Component<any, IExploreState> {
+export class Explore extends React.Component<any, ExploreState> {
   el: any;
 
   constructor(props) {
     super(props);
-    const initialState: IExploreState = props.initialState;
+    const initialState: ExploreState = props.initialState;
     const { datasource, queries, range } = parseUrlState(props.routeParams.state);
     this.state = {
       datasource: null,
diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx
index aac5d32750a..072908d2b8e 100644
--- a/public/app/containers/ManageDashboards/FolderPermissions.tsx
+++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
 import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import { toJS } from 'mobx';
-import IContainerProps from 'app/containers/IContainerProps';
+import ContainerProps from 'app/containers/ContainerProps';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import Permissions from 'app/core/components/Permissions/Permissions';
 import Tooltip from 'app/core/components/Tooltip/Tooltip';
@@ -12,7 +12,7 @@ import SlideDown from 'app/core/components/Animations/SlideDown';
 
 @inject('nav', 'folder', 'view', 'permissions')
 @observer
-export class FolderPermissions extends Component<IContainerProps, any> {
+export class FolderPermissions extends Component<ContainerProps, any> {
   constructor(props) {
     super(props);
     this.handleAddPermission = this.handleAddPermission.bind(this);
diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx
index d25a7dc6c06..88830356563 100644
--- a/public/app/containers/ManageDashboards/FolderSettings.tsx
+++ b/public/app/containers/ManageDashboards/FolderSettings.tsx
@@ -3,13 +3,13 @@ import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import { toJS } from 'mobx';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import IContainerProps from 'app/containers/IContainerProps';
+import ContainerProps from 'app/containers/ContainerProps';
 import { getSnapshot } from 'mobx-state-tree';
 import appEvents from 'app/core/app_events';
 
 @inject('nav', 'folder', 'view')
 @observer
-export class FolderSettings extends React.Component<IContainerProps, any> {
+export class FolderSettings extends React.Component<ContainerProps, any> {
   formSnapshot: any;
 
   componentDidMount() {
diff --git a/public/app/containers/ServerStats/ServerStats.tsx b/public/app/containers/ServerStats/ServerStats.tsx
index 761b296855f..63e78996041 100644
--- a/public/app/containers/ServerStats/ServerStats.tsx
+++ b/public/app/containers/ServerStats/ServerStats.tsx
@@ -2,11 +2,11 @@ import React from 'react';
 import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import IContainerProps from 'app/containers/IContainerProps';
+import ContainerProps from 'app/containers/ContainerProps';
 
 @inject('nav', 'serverStats')
 @observer
-export class ServerStats extends React.Component<IContainerProps, any> {
+export class ServerStats extends React.Component<ContainerProps, any> {
   constructor(props) {
     super(props);
     const { nav, serverStats } = this.props;
diff --git a/public/app/containers/Teams/TeamGroupSync.tsx b/public/app/containers/Teams/TeamGroupSync.tsx
index 323dceae0d8..a3b2e4aed14 100644
--- a/public/app/containers/Teams/TeamGroupSync.tsx
+++ b/public/app/containers/Teams/TeamGroupSync.tsx
@@ -1,12 +1,12 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
 import { observer } from 'mobx-react';
-import { ITeam, ITeamGroup } from 'app/stores/TeamsStore/TeamsStore';
+import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import Tooltip from 'app/core/components/Tooltip/Tooltip';
 
 interface Props {
-  team: ITeam;
+  team: Team;
 }
 
 interface State {
@@ -27,7 +27,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
     this.props.team.loadGroups();
   }
 
-  renderGroup(group: ITeamGroup) {
+  renderGroup(group: TeamGroup) {
     return (
       <tr key={group.groupId}>
         <td>{group.groupId}</td>
@@ -53,7 +53,7 @@ export class TeamGroupSync extends React.Component<Props, State> {
     this.setState({ isAdding: false, newGroupId: '' });
   };
 
-  onRemoveGroup = (group: ITeamGroup) => {
+  onRemoveGroup = (group: TeamGroup) => {
     this.props.team.removeGroup(group.groupId);
   };
 
diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx
index 31406250cb3..2d037eed642 100644
--- a/public/app/containers/Teams/TeamList.tsx
+++ b/public/app/containers/Teams/TeamList.tsx
@@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import { NavStore } from 'app/stores/NavStore/NavStore';
-import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore';
+import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
 import { BackendSrv } from 'app/core/services/backend_srv';
 import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
 
@@ -27,7 +27,7 @@ export class TeamList extends React.Component<Props, any> {
     this.props.teams.loadTeams();
   }
 
-  deleteTeam(team: ITeam) {
+  deleteTeam(team: Team) {
     this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this));
   }
 
@@ -35,7 +35,7 @@ export class TeamList extends React.Component<Props, any> {
     this.props.teams.setSearchQuery(evt.target.value);
   };
 
-  renderTeamMember(team: ITeam): JSX.Element {
+  renderTeamMember(team: Team): JSX.Element {
     let teamUrl = `org/teams/edit/${team.id}`;
 
     return (
diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/containers/Teams/TeamMembers.tsx
index a6b0b04f19d..b06a547063a 100644
--- a/public/app/containers/Teams/TeamMembers.tsx
+++ b/public/app/containers/Teams/TeamMembers.tsx
@@ -1,13 +1,13 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
 import { observer } from 'mobx-react';
-import { ITeam, ITeamMember } from 'app/stores/TeamsStore/TeamsStore';
+import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
 import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
 
 interface Props {
-  team: ITeam;
+  team: Team;
 }
 
 interface State {
@@ -30,15 +30,15 @@ export class TeamMembers extends React.Component<Props, State> {
     this.props.team.setSearchQuery(evt.target.value);
   };
 
-  removeMember(member: ITeamMember) {
+  removeMember(member: TeamMember) {
     this.props.team.removeMember(member);
   }
 
-  removeMemberConfirmed(member: ITeamMember) {
+  removeMemberConfirmed(member: TeamMember) {
     this.props.team.removeMember(member);
   }
 
-  renderMember(member: ITeamMember) {
+  renderMember(member: TeamMember) {
     return (
       <tr key={member.userId}>
         <td className="width-4 text-center">
diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/containers/Teams/TeamPages.tsx
index 500a7cbe5e8..2abc9c51535 100644
--- a/public/app/containers/Teams/TeamPages.tsx
+++ b/public/app/containers/Teams/TeamPages.tsx
@@ -5,7 +5,7 @@ import { inject, observer } from 'mobx-react';
 import config from 'app/core/config';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import { NavStore } from 'app/stores/NavStore/NavStore';
-import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore';
+import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
 import { ViewStore } from 'app/stores/ViewStore/ViewStore';
 import TeamMembers from './TeamMembers';
 import TeamSettings from './TeamSettings';
@@ -40,7 +40,7 @@ export class TeamPages extends React.Component<Props, any> {
     nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled);
   }
 
-  getCurrentTeam(): ITeam {
+  getCurrentTeam(): Team {
     const { teams, view } = this.props;
     return teams.map.get(view.routeParams.get('id'));
   }
diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/containers/Teams/TeamSettings.tsx
index 142088a5d1e..0de60a0b16c 100644
--- a/public/app/containers/Teams/TeamSettings.tsx
+++ b/public/app/containers/Teams/TeamSettings.tsx
@@ -1,11 +1,11 @@
 import React from 'react';
 import { hot } from 'react-hot-loader';
 import { observer } from 'mobx-react';
-import { ITeam } from 'app/stores/TeamsStore/TeamsStore';
+import { Team } from 'app/stores/TeamsStore/TeamsStore';
 import { Label } from 'app/core/components/Forms/Forms';
 
 interface Props {
-  team: ITeam;
+  team: Team;
 }
 
 @observer
diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
index 1583303dfa1..5ece360e36a 100644
--- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
+++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
@@ -1,34 +1,37 @@
 import React, { Component } from 'react';
 
-export interface IProps {
-    model: any;
+export interface Props {
+  model: any;
 }
 
-class EmptyListCTA extends Component<IProps, any> {
-    render() {
-        const {
-            title,
-            buttonIcon,
-            buttonLink,
-            buttonTitle,
-            proTip,
-            proTipLink,
-            proTipLinkTitle,
-            proTipTarget
-        } = this.props.model;
-        return (
-            <div className="empty-list-cta">
-                <div className="empty-list-cta__title">{title}</div>
-                <a href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success"><i className={buttonIcon} />{buttonTitle}</a>
-                <div className="empty-list-cta__pro-tip">
-                    <i className="fa fa-rocket" /> ProTip: {proTip}
-                    <a className="text-link empty-list-cta__pro-tip-link"
-                        href={proTipLink}
-                        target={proTipTarget}>{proTipLinkTitle}</a>
-                </div>
-            </div>
-        );
-    }
+class EmptyListCTA extends Component<Props, any> {
+  render() {
+    const {
+      title,
+      buttonIcon,
+      buttonLink,
+      buttonTitle,
+      proTip,
+      proTipLink,
+      proTipLinkTitle,
+      proTipTarget,
+    } = this.props.model;
+    return (
+      <div className="empty-list-cta">
+        <div className="empty-list-cta__title">{title}</div>
+        <a href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
+          <i className={buttonIcon} />
+          {buttonTitle}
+        </a>
+        <div className="empty-list-cta__pro-tip">
+          <i className="fa fa-rocket" /> ProTip: {proTip}
+          <a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
+            {proTipLinkTitle}
+          </a>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default EmptyListCTA;
diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx
index f998cb9981f..1d744b7e609 100644
--- a/public/app/core/components/PageHeader/PageHeader.tsx
+++ b/public/app/core/components/PageHeader/PageHeader.tsx
@@ -5,7 +5,7 @@ import classNames from 'classnames';
 import appEvents from 'app/core/app_events';
 import { toJS } from 'mobx';
 
-export interface IProps {
+export interface Props {
   model: NavModel;
 }
 
@@ -82,7 +82,7 @@ const Navigation = ({ main }: { main: NavModelItem }) => {
 };
 
 @observer
-export default class PageHeader extends React.Component<IProps, any> {
+export default class PageHeader extends React.Component<Props, any> {
   constructor(props) {
     super(props);
   }
diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx
index 8f92b18445c..1d676a00a37 100644
--- a/public/app/core/components/PasswordStrength.tsx
+++ b/public/app/core/components/PasswordStrength.tsx
@@ -1,32 +1,31 @@
 import React from 'react';
 
-export interface IProps {
+export interface Props {
   password: string;
 }
 
-export class PasswordStrength extends React.Component<IProps, any> {
-
+export class PasswordStrength extends React.Component<Props, any> {
   constructor(props) {
     super(props);
   }
 
   render() {
     const { password } = this.props;
-    let strengthText = "strength: strong like a bull.";
-    let strengthClass = "password-strength-good";
+    let strengthText = 'strength: strong like a bull.';
+    let strengthClass = 'password-strength-good';
 
     if (!password) {
       return null;
     }
 
     if (password.length <= 8) {
-      strengthText = "strength: you can do better.";
-      strengthClass = "password-strength-ok";
+      strengthText = 'strength: you can do better.';
+      strengthClass = 'password-strength-ok';
     }
 
     if (password.length < 4) {
-      strengthText = "strength: weak sauce.";
-      strengthClass = "password-strength-bad";
+      strengthText = 'strength: weak sauce.';
+      strengthClass = 'password-strength-bad';
     }
 
     return (
@@ -36,5 +35,3 @@ export class PasswordStrength extends React.Component<IProps, any> {
     );
   }
 }
-
-
diff --git a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx
index bbb9754fe0d..d65595dae66 100644
--- a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx
+++ b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx
@@ -2,11 +2,11 @@ import React, { Component } from 'react';
 import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
 import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
 
-export interface IProps {
+export interface Props {
   item: any;
 }
 
-export default class DisabledPermissionListItem extends Component<IProps, any> {
+export default class DisabledPermissionListItem extends Component<Props, any> {
   render() {
     const { item } = this.props;
 
diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx
index dbdc1682f6b..d17899c891f 100644
--- a/public/app/core/components/Permissions/Permissions.tsx
+++ b/public/app/core/components/Permissions/Permissions.tsx
@@ -20,7 +20,7 @@ export interface DashboardAcl {
   sortRank?: number;
 }
 
-export interface IProps {
+export interface Props {
   dashboardId: number;
   folderInfo?: FolderInfo;
   permissions?: any;
@@ -29,7 +29,7 @@ export interface IProps {
 }
 
 @observer
-class Permissions extends Component<IProps, any> {
+class Permissions extends Component<Props, any> {
   constructor(props) {
     super(props);
     const { dashboardId, isFolder, folderInfo } = this.props;
diff --git a/public/app/core/components/Permissions/PermissionsList.tsx b/public/app/core/components/Permissions/PermissionsList.tsx
index a77235ecc30..7e64de012e4 100644
--- a/public/app/core/components/Permissions/PermissionsList.tsx
+++ b/public/app/core/components/Permissions/PermissionsList.tsx
@@ -4,7 +4,7 @@ import DisabledPermissionsListItem from './DisabledPermissionsListItem';
 import { observer } from 'mobx-react';
 import { FolderInfo } from './FolderInfo';
 
-export interface IProps {
+export interface Props {
   permissions: any[];
   removeItem: any;
   permissionChanged: any;
@@ -13,7 +13,7 @@ export interface IProps {
 }
 
 @observer
-class PermissionsList extends Component<IProps, any> {
+class PermissionsList extends Component<Props, any> {
   render() {
     const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props;
 
diff --git a/public/app/core/components/Picker/DescriptionOption.tsx b/public/app/core/components/Picker/DescriptionOption.tsx
index 12a1fdd9163..1bcb7100489 100644
--- a/public/app/core/components/Picker/DescriptionOption.tsx
+++ b/public/app/core/components/Picker/DescriptionOption.tsx
@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 
-export interface IProps {
+export interface Props {
   onSelect: any;
   onFocus: any;
   option: any;
@@ -8,7 +8,7 @@ export interface IProps {
   className: any;
 }
 
-class DescriptionOption extends Component<IProps, any> {
+class DescriptionOption extends Component<Props, any> {
   constructor(props) {
     super(props);
     this.handleMouseDown = this.handleMouseDown.bind(this);
diff --git a/public/app/core/components/Picker/PickerOption.tsx b/public/app/core/components/Picker/PickerOption.tsx
index 1b32adac572..f30a7c06d10 100644
--- a/public/app/core/components/Picker/PickerOption.tsx
+++ b/public/app/core/components/Picker/PickerOption.tsx
@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 
-export interface IProps {
+export interface Props {
   onSelect: any;
   onFocus: any;
   option: any;
@@ -8,7 +8,7 @@ export interface IProps {
   className: any;
 }
 
-class UserPickerOption extends Component<IProps, any> {
+class UserPickerOption extends Component<Props, any> {
   constructor(props) {
     super(props);
     this.handleMouseDown = this.handleMouseDown.bind(this);
diff --git a/public/app/core/components/TagFilter/TagBadge.tsx b/public/app/core/components/TagFilter/TagBadge.tsx
index e5c2e357a58..d93b5fd1e74 100644
--- a/public/app/core/components/TagFilter/TagBadge.tsx
+++ b/public/app/core/components/TagFilter/TagBadge.tsx
@@ -1,14 +1,14 @@
 import React from 'react';
 import tags from 'app/core/utils/tags';
 
-export interface IProps {
+export interface Props {
   label: string;
   removeIcon: boolean;
   count: number;
   onClick: any;
 }
 
-export class TagBadge extends React.Component<IProps, any> {
+export class TagBadge extends React.Component<Props, any> {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx
index 0b6058f3dd2..84f3e1819cd 100644
--- a/public/app/core/components/TagFilter/TagFilter.tsx
+++ b/public/app/core/components/TagFilter/TagFilter.tsx
@@ -4,13 +4,13 @@ import { Async } from 'react-select';
 import { TagValue } from './TagValue';
 import { TagOption } from './TagOption';
 
-export interface IProps {
+export interface Props {
   tags: string[];
   tagOptions: () => any;
   onSelect: (tag: string) => void;
 }
 
-export class TagFilter extends React.Component<IProps, any> {
+export class TagFilter extends React.Component<Props, any> {
   inlineTags: boolean;
 
   constructor(props) {
diff --git a/public/app/core/components/TagFilter/TagOption.tsx b/public/app/core/components/TagFilter/TagOption.tsx
index 402544dd5f3..5938c98f870 100644
--- a/public/app/core/components/TagFilter/TagOption.tsx
+++ b/public/app/core/components/TagFilter/TagOption.tsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import { TagBadge } from './TagBadge';
 
-export interface IProps {
+export interface Props {
   onSelect: any;
   onFocus: any;
   option: any;
@@ -9,7 +9,7 @@ export interface IProps {
   className: any;
 }
 
-export class TagOption extends React.Component<IProps, any> {
+export class TagOption extends React.Component<Props, any> {
   constructor(props) {
     super(props);
     this.handleMouseDown = this.handleMouseDown.bind(this);
diff --git a/public/app/core/components/TagFilter/TagValue.tsx b/public/app/core/components/TagFilter/TagValue.tsx
index 2e7819951f2..ca8ca9e4fba 100644
--- a/public/app/core/components/TagFilter/TagValue.tsx
+++ b/public/app/core/components/TagFilter/TagValue.tsx
@@ -1,14 +1,14 @@
 import React from 'react';
 import { TagBadge } from './TagBadge';
 
-export interface IProps {
+export interface Props {
   value: any;
   className: any;
   onClick: any;
   onRemove: any;
 }
 
-export class TagValue extends React.Component<IProps, any> {
+export class TagValue extends React.Component<Props, any> {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
diff --git a/public/app/core/components/Tooltip/Popover.tsx b/public/app/core/components/Tooltip/Popover.tsx
index 4dc25d34130..ee86d07fb53 100644
--- a/public/app/core/components/Tooltip/Popover.tsx
+++ b/public/app/core/components/Tooltip/Popover.tsx
@@ -2,11 +2,11 @@
 import withTooltip from './withTooltip';
 import { Target } from 'react-popper';
 
-interface IPopoverProps {
+interface PopoverProps {
   tooltipSetState: (prevState: object) => void;
 }
 
-class Popover extends React.Component<IPopoverProps, any> {
+class Popover extends React.Component<PopoverProps, any> {
   constructor(props) {
     super(props);
     this.toggleTooltip = this.toggleTooltip.bind(this);
diff --git a/public/app/core/components/Tooltip/Tooltip.tsx b/public/app/core/components/Tooltip/Tooltip.tsx
index ae4093ea3f1..a265c8487d3 100644
--- a/public/app/core/components/Tooltip/Tooltip.tsx
+++ b/public/app/core/components/Tooltip/Tooltip.tsx
@@ -2,11 +2,11 @@
 import withTooltip from './withTooltip';
 import { Target } from 'react-popper';
 
-interface ITooltipProps {
+interface TooltipProps {
   tooltipSetState: (prevState: object) => void;
 }
 
-class Tooltip extends React.Component<ITooltipProps, any> {
+class Tooltip extends React.Component<TooltipProps, any> {
   constructor(props) {
     super(props);
     this.showTooltip = this.showTooltip.bind(this);
diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/public/app/core/components/colorpicker/ColorPalette.tsx
index 07b25a32046..edb2629d16d 100644
--- a/public/app/core/components/colorpicker/ColorPalette.tsx
+++ b/public/app/core/components/colorpicker/ColorPalette.tsx
@@ -1,12 +1,12 @@
 import React from 'react';
 import { sortedColors } from 'app/core/utils/colors';
 
-export interface IProps {
+export interface Props {
   color: string;
   onColorSelect: (c: string) => void;
 }
 
-export class ColorPalette extends React.Component<IProps, any> {
+export class ColorPalette extends React.Component<Props, any> {
   paletteColors: string[];
 
   constructor(props) {
@@ -29,7 +29,8 @@ export class ColorPalette extends React.Component<IProps, any> {
           key={paletteColor}
           className={'pointer fa ' + cssClass}
           style={{ color: paletteColor }}
-          onClick={this.onColorSelect(paletteColor)}>
+          onClick={this.onColorSelect(paletteColor)}
+        >
           &nbsp;
         </i>
       );
@@ -41,4 +42,3 @@ export class ColorPalette extends React.Component<IProps, any> {
     );
   }
 }
-
diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx
index dbba75636d0..c492d3829ca 100644
--- a/public/app/core/components/colorpicker/ColorPicker.tsx
+++ b/public/app/core/components/colorpicker/ColorPicker.tsx
@@ -5,12 +5,12 @@ import Drop from 'tether-drop';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 
-export interface IProps {
+export interface Props {
   color: string;
   onChange: (c: string) => void;
 }
 
-export class ColorPicker extends React.Component<IProps, any> {
+export class ColorPicker extends React.Component<Props, any> {
   pickerElem: any;
   colorPickerDrop: any;
 
diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx
index 360c3fdd5c4..ac7dd6a2738 100644
--- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx
+++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx
@@ -6,12 +6,12 @@ import { SpectrumPicker } from './SpectrumPicker';
 
 const DEFAULT_COLOR = '#000000';
 
-export interface IProps {
+export interface Props {
   color: string;
   onColorSelect: (c: string) => void;
 }
 
-export class ColorPickerPopover extends React.Component<IProps, any> {
+export class ColorPickerPopover extends React.Component<Props, any> {
   pickerNavElem: any;
 
   constructor(props) {
@@ -19,7 +19,7 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
     this.state = {
       tab: 'palette',
       color: this.props.color || DEFAULT_COLOR,
-      colorString: this.props.color || DEFAULT_COLOR
+      colorString: this.props.color || DEFAULT_COLOR,
     };
   }
 
@@ -32,7 +32,7 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
     if (newColor.isValid()) {
       this.setState({
         color: newColor.toString(),
-        colorString: newColor.toString()
+        colorString: newColor.toString(),
       });
       this.props.onColorSelect(color);
     }
@@ -50,7 +50,7 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
   onColorStringChange(e) {
     let colorString = e.target.value;
     this.setState({
-      colorString: colorString
+      colorString: colorString,
     });
 
     let newColor = tinycolor(colorString);
@@ -71,11 +71,11 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
 
   componentDidMount() {
     this.pickerNavElem.find('li:first').addClass('active');
-    this.pickerNavElem.on('show', (e) => {
+    this.pickerNavElem.on('show', e => {
       // use href attr (#name => name)
       let tab = e.target.hash.slice(1);
       this.setState({
-        tab: tab
+        tab: tab,
       });
     });
   }
@@ -97,19 +97,24 @@ export class ColorPickerPopover extends React.Component<IProps, any> {
       <div className="gf-color-picker">
         <ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem.bind(this)}>
           <li className="gf-tabs-item-colorpicker">
-            <a href="#palette" data-toggle="tab">Colors</a>
+            <a href="#palette" data-toggle="tab">
+              Colors
+            </a>
           </li>
           <li className="gf-tabs-item-colorpicker">
-            <a href="#spectrum" data-toggle="tab">Custom</a>
+            <a href="#spectrum" data-toggle="tab">
+              Custom
+            </a>
           </li>
         </ul>
-        <div className="gf-color-picker__body">
-          {currentTab}
-        </div>
+        <div className="gf-color-picker__body">{currentTab}</div>
         <div>
-          <input className="gf-form-input gf-form-input--small" value={this.state.colorString}
-            onChange={this.onColorStringChange.bind(this)} onBlur={this.onColorStringBlur.bind(this)}>
-          </input>
+          <input
+            className="gf-form-input gf-form-input--small"
+            value={this.state.colorString}
+            onChange={this.onColorStringChange.bind(this)}
+            onBlur={this.onColorStringBlur.bind(this)}
+          />
         </div>
       </div>
     );
diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx
index 3b24b9a4661..b514899e2e2 100644
--- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx
+++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx
@@ -2,13 +2,13 @@ import React from 'react';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 
-export interface IProps {
+export interface Props {
   series: any;
   onColorChange: (color: string) => void;
   onToggleAxis: () => void;
 }
 
-export class SeriesColorPicker extends React.Component<IProps, any> {
+export class SeriesColorPicker extends React.Component<Props, any> {
   constructor(props) {
     super(props);
     this.onColorChange = this.onColorChange.bind(this);
diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx
index eef04545308..e8a30e8c460 100644
--- a/public/app/core/components/colorpicker/SpectrumPicker.tsx
+++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx
@@ -3,13 +3,13 @@ import _ from 'lodash';
 import $ from 'jquery';
 import 'vendor/spectrum';
 
-export interface IProps {
+export interface Props {
   color: string;
   options: object;
   onColorSelect: (c: string) => void;
 }
 
-export class SpectrumPicker extends React.Component<IProps, any> {
+export class SpectrumPicker extends React.Component<Props, any> {
   elem: any;
   isMoving: boolean;
 
@@ -29,14 +29,17 @@ export class SpectrumPicker extends React.Component<IProps, any> {
   }
 
   componentDidMount() {
-    let spectrumOptions = _.assignIn({
-      flat: true,
-      showAlpha: true,
-      showButtons: false,
-      color: this.props.color,
-      appendTo: this.elem,
-      move: this.onSpectrumMove,
-    }, this.props.options);
+    let spectrumOptions = _.assignIn(
+      {
+        flat: true,
+        showAlpha: true,
+        showButtons: false,
+        color: this.props.color,
+        appendTo: this.elem,
+        move: this.onSpectrumMove,
+      },
+      this.props.options
+    );
 
     this.elem.spectrum(spectrumOptions);
     this.elem.spectrum('show');
@@ -64,9 +67,6 @@ export class SpectrumPicker extends React.Component<IProps, any> {
   }
 
   render() {
-    return (
-      <div className="spectrum-container" ref={this.setComponentElem}></div>
-    );
+    return <div className="spectrum-container" ref={this.setComponentElem} />;
   }
 }
-
diff --git a/public/app/stores/AlertListStore/AlertListStore.ts b/public/app/stores/AlertListStore/AlertListStore.ts
index 7d60ce04180..ec27565a1a1 100644
--- a/public/app/stores/AlertListStore/AlertListStore.ts
+++ b/public/app/stores/AlertListStore/AlertListStore.ts
@@ -1,13 +1,13 @@
 import { types, getEnv, flow } from 'mobx-state-tree';
-import { AlertRule } from './AlertRule';
+import { AlertRule as AlertRuleModel } from './AlertRule';
 import { setStateFields } from './helpers';
 
-type IAlertRuleType = typeof AlertRule.Type;
-export interface IAlertRule extends IAlertRuleType {}
+type AlertRuleType = typeof AlertRuleModel.Type;
+export interface AlertRule extends AlertRuleType {}
 
 export const AlertListStore = types
   .model('AlertListStore', {
-    rules: types.array(AlertRule),
+    rules: types.array(AlertRuleModel),
     stateFilter: types.optional(types.string, 'all'),
     search: types.optional(types.string, ''),
   })
@@ -38,7 +38,7 @@ export const AlertListStore = types
           }
         }
 
-        self.rules.push(AlertRule.create(rule));
+        self.rules.push(AlertRuleModel.create(rule));
       }
     }),
     setSearchQuery(query: string) {
diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts
index c69c32befa8..bef53b828b6 100644
--- a/public/app/stores/NavStore/NavStore.ts
+++ b/public/app/stores/NavStore/NavStore.ts
@@ -1,7 +1,7 @@
 import _ from 'lodash';
 import { types, getEnv } from 'mobx-state-tree';
 import { NavItem } from './NavItem';
-import { ITeam } from '../TeamsStore/TeamsStore';
+import { Team } from '../TeamsStore/TeamsStore';
 
 export const NavStore = types
   .model('NavStore', {
@@ -117,7 +117,7 @@ export const NavStore = types
       self.main = NavItem.create(main);
     },
 
-    initTeamPage(team: ITeam, tab: string, isSyncEnabled: boolean) {
+    initTeamPage(team: Team, tab: string, isSyncEnabled: boolean) {
       let main = {
         img: team.avatarUrl,
         id: 'team-' + team.id,
diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts
index 8a915d20ef1..bb85a85d9dd 100644
--- a/public/app/stores/RootStore/RootStore.ts
+++ b/public/app/stores/RootStore/RootStore.ts
@@ -34,5 +34,5 @@ export const RootStore = types.model({
   }),
 });
 
-type IRootStoreType = typeof RootStore.Type;
-export interface IRootStore extends IRootStoreType {}
+type RootStoreType = typeof RootStore.Type;
+export interface RootStoreInterface extends RootStoreType {}
diff --git a/public/app/stores/TeamsStore/TeamsStore.ts b/public/app/stores/TeamsStore/TeamsStore.ts
index 01cdca895d4..1aec4a1433c 100644
--- a/public/app/stores/TeamsStore/TeamsStore.ts
+++ b/public/app/stores/TeamsStore/TeamsStore.ts
@@ -1,6 +1,6 @@
 import { types, getEnv, flow } from 'mobx-state-tree';
 
-export const TeamMember = types.model('TeamMember', {
+export const TeamMemberModel = types.model('TeamMember', {
   userId: types.identifier(types.number),
   teamId: types.number,
   avatarUrl: types.string,
@@ -8,18 +8,18 @@ export const TeamMember = types.model('TeamMember', {
   login: types.string,
 });
 
-type TeamMemberType = typeof TeamMember.Type;
-export interface ITeamMember extends TeamMemberType {}
+type TeamMemberType = typeof TeamMemberModel.Type;
+export interface TeamMember extends TeamMemberType {}
 
-export const TeamGroup = types.model('TeamGroup', {
+export const TeamGroupModel = types.model('TeamGroup', {
   groupId: types.identifier(types.string),
   teamId: types.number,
 });
 
-type TeamGroupType = typeof TeamGroup.Type;
-export interface ITeamGroup extends TeamGroupType {}
+type TeamGroupType = typeof TeamGroupModel.Type;
+export interface TeamGroup extends TeamGroupType {}
 
-export const Team = types
+export const TeamModel = types
   .model('Team', {
     id: types.identifier(types.number),
     name: types.string,
@@ -27,8 +27,8 @@ export const Team = types
     email: types.string,
     memberCount: types.number,
     search: types.optional(types.string, ''),
-    members: types.optional(types.map(TeamMember), {}),
-    groups: types.optional(types.map(TeamGroup), {}),
+    members: types.optional(types.map(TeamMemberModel), {}),
+    groups: types.optional(types.map(TeamGroupModel), {}),
   })
   .views(self => ({
     get filteredMembers() {
@@ -67,11 +67,11 @@ export const Team = types
       self.members.clear();
 
       for (let member of rsp) {
-        self.members.set(member.userId.toString(), TeamMember.create(member));
+        self.members.set(member.userId.toString(), TeamMemberModel.create(member));
       }
     }),
 
-    removeMember: flow(function* load(member: ITeamMember) {
+    removeMember: flow(function* load(member: TeamMember) {
       const backendSrv = getEnv(self).backendSrv;
       yield backendSrv.delete(`/api/teams/${self.id}/members/${member.userId}`);
       // remove from store map
@@ -89,7 +89,7 @@ export const Team = types
       self.groups.clear();
 
       for (let group of rsp) {
-        self.groups.set(group.groupId, TeamGroup.create(group));
+        self.groups.set(group.groupId, TeamGroupModel.create(group));
       }
     }),
 
@@ -98,7 +98,7 @@ export const Team = types
       yield backendSrv.post(`/api/teams/${self.id}/groups`, { groupId: groupId });
       self.groups.set(
         groupId,
-        TeamGroup.create({
+        TeamGroupModel.create({
           teamId: self.id,
           groupId: groupId,
         })
@@ -112,12 +112,12 @@ export const Team = types
     }),
   }));
 
-type TeamType = typeof Team.Type;
-export interface ITeam extends TeamType {}
+type TeamType = typeof TeamModel.Type;
+export interface Team extends TeamType {}
 
 export const TeamsStore = types
   .model('TeamsStore', {
-    map: types.map(Team),
+    map: types.map(TeamModel),
     search: types.optional(types.string, ''),
   })
   .views(self => ({
@@ -136,7 +136,7 @@ export const TeamsStore = types
       self.map.clear();
 
       for (let team of rsp.teams) {
-        self.map.set(team.id.toString(), Team.create(team));
+        self.map.set(team.id.toString(), TeamModel.create(team));
       }
     }),
 
@@ -151,6 +151,6 @@ export const TeamsStore = types
 
       const backendSrv = getEnv(self).backendSrv;
       const team = yield backendSrv.get(`/api/teams/${id}`);
-      self.map.set(id, Team.create(team));
+      self.map.set(id, TeamModel.create(team));
     }),
   }));
diff --git a/public/app/stores/store.ts b/public/app/stores/store.ts
index dfbd8141198..10acbfe4907 100644
--- a/public/app/stores/store.ts
+++ b/public/app/stores/store.ts
@@ -1,7 +1,7 @@
-import { RootStore, IRootStore } from './RootStore/RootStore';
+import { RootStore, RootStoreInterface } from './RootStore/RootStore';
 import config from 'app/core/config';
 
-export let store: IRootStore;
+export let store: RootStoreInterface;
 
 export function createStore(services) {
   store = RootStore.create(
diff --git a/tslint.json b/tslint.json
index 22e123e0364..9a72f9ccebc 100644
--- a/tslint.json
+++ b/tslint.json
@@ -1,5 +1,6 @@
 {
   "rules": {
+    "interface-name": [true, "never-prefix"],
     "no-string-throw": true,
     "no-unused-expression": true,
     "no-unused-variable": false,
diff --git a/yarn.lock b/yarn.lock
index dd1cde4e698..fb593043288 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11454,6 +11454,12 @@ tslint-loader@^3.5.3:
     rimraf "^2.4.4"
     semver "^5.3.0"
 
+tslint-react@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.6.0.tgz#7f462c95c4a0afaae82507f06517ff02942196a1"
+  dependencies:
+    tsutils "^2.13.1"
+
 tslint@^5.8.0:
   version "5.10.0"
   resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3"
@@ -11477,6 +11483,12 @@ tsutils@^2.12.1:
   dependencies:
     tslib "^1.8.1"
 
+tsutils@^2.13.1:
+  version "2.29.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
+  dependencies:
+    tslib "^1.8.1"
+
 tty-browserify@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"