From d83f886519c1e20d3eadfa03a575ed8d0022cdea Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 3 Apr 2018 16:36:43 +0200 Subject: [PATCH 001/410] 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 = ''; - - 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 = ''; - - 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 - * Joel Oughton - * Nicolas Joseph - * - * 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 = $('
').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 = $('
').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 = $('
').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 = $('
').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 = ''; + + 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 = ''; + + 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 + * Joel Oughton + * Nicolas Joseph + * + * 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 = $('
') + .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 = $('
').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 = $('
').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 = $('
').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 Date: Tue, 10 Apr 2018 14:16:56 +0200 Subject: [PATCH 002/410] 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 = ''; @@ -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 = $('
').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 0425b47791febe064b33c2ef2e75a7e6217877c7 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 11 Jun 2018 17:36:45 +0200 Subject: [PATCH 003/410] refactor Explore query field --- public/app/containers/Explore/QueryField.tsx | 26 ++-- public/app/containers/Explore/QueryRows.tsx | 4 + .../Explore/slate-plugins/prism/index.tsx | 21 +-- public/sass/_grafana.scss | 1 + public/sass/components/_slate_editor.scss | 146 +++++++++++++++++ public/sass/pages/_explore.scss | 147 ------------------ 6 files changed, 178 insertions(+), 167 deletions(-) create mode 100644 public/sass/components/_slate_editor.scss diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index 53354584fea..c0d51ad9cfc 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -9,7 +9,7 @@ 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, { configurePrismMetricsTokens } from './slate-plugins/prism/index'; +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'; @@ -17,13 +17,13 @@ import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus'; import Typeahead from './Typeahead'; const EMPTY_METRIC = ''; -const TYPEAHEAD_DEBOUNCE = 300; +export const TYPEAHEAD_DEBOUNCE = 300; function flattenSuggestions(s) { return s ? s.reduce((acc, g) => acc.concat(g.items), []) : []; } -const getInitialValue = query => +export const getInitialValue = query => Value.fromJSON({ document: { nodes: [ @@ -45,12 +45,14 @@ const getInitialValue = query => }, }); -class Portal extends React.Component { +class Portal extends React.Component { node: any; + constructor(props) { super(props); + const { index = 0, prefix = 'query' } = props; this.node = document.createElement('div'); - this.node.classList.add('explore-typeahead', `explore-typeahead-${props.index}`); + this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`); document.body.appendChild(this.node); } @@ -71,12 +73,14 @@ class QueryField extends React.Component { constructor(props, context) { super(props, context); + const { prismDefinition = {}, prismLanguage = 'promql' } = props; + this.plugins = [ BracesPlugin(), ClearPlugin(), RunnerPlugin({ handler: props.onPressEnter }), NewlinePlugin(), - PluginPrism(), + PluginPrism({ definition: prismDefinition, language: prismLanguage }), ]; this.state = { @@ -131,7 +135,8 @@ class QueryField extends React.Component { if (!this.state.metrics) { return; } - configurePrismMetricsTokens(this.state.metrics); + setPrismTokens(this.props.language, 'metrics', this.state.metrics); + // Trigger re-render window.requestAnimationFrame(() => { // Bogus edit to trigger highlighting @@ -162,7 +167,7 @@ class QueryField extends React.Component { const selection = window.getSelection(); if (selection.anchorNode) { const wrapperNode = selection.anchorNode.parentElement; - const editorNode = wrapperNode.closest('.query-field'); + const editorNode = wrapperNode.closest('.slate-query-field'); if (!editorNode || this.state.value.isBlurred) { // Not inside this editor return; @@ -514,6 +519,7 @@ class QueryField extends React.Component { }; renderMenu = () => { + const { portalPrefix } = this.props; const { suggestions } = this.state; const hasSuggesstions = suggestions && suggestions.length > 0; if (!hasSuggesstions) { @@ -528,7 +534,7 @@ class QueryField extends React.Component { // Create typeahead in DOM root so we can later position it absolutely return ( - + { render() { return ( -
+
{this.renderMenu()} { @@ -58,9 +59,12 @@ class QueryRow extends PureComponent {
diff --git a/public/app/containers/Explore/slate-plugins/prism/index.tsx b/public/app/containers/Explore/slate-plugins/prism/index.tsx index 7c3fa296d8e..d185518790f 100644 --- a/public/app/containers/Explore/slate-plugins/prism/index.tsx +++ b/public/app/containers/Explore/slate-plugins/prism/index.tsx @@ -1,16 +1,12 @@ import React from 'react'; import Prism from 'prismjs'; -import Promql from './promql'; - -Prism.languages.promql = Promql; - const TOKEN_MARK = 'prism-token'; -export function configurePrismMetricsTokens(metrics) { - Prism.languages.promql.metric = { - alias: 'variable', - pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`), +export function setPrismTokens(language, field, values, alias = 'variable') { + Prism.languages[language][field] = { + alias, + pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`), }; } @@ -21,7 +17,12 @@ export function configurePrismMetricsTokens(metrics) { * (Adapted to handle nested grammar definitions.) */ -export default function PrismPlugin() { +export default function PrismPlugin({ definition, language }) { + if (definition) { + // Don't override exising modified definitions + Prism.languages[language] = Prism.languages[language] || definition; + } + return { /** * Render a Slate mark with appropiate CSS class names @@ -54,7 +55,7 @@ export default function PrismPlugin() { const texts = node.getTexts().toArray(); const tstring = texts.map(t => t.text).join('\n'); - const grammar = Prism.languages.promql; + const grammar = Prism.languages[language]; const tokens = Prism.tokenize(tstring, grammar); const decorations = []; let startText = texts.shift(); diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index afc869f8b15..9e3bec267ed 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -67,6 +67,7 @@ @import 'components/filter-list'; @import 'components/filter-table'; @import 'components/old_stuff'; +@import 'components/slate_editor'; @import 'components/typeahead'; @import 'components/modals'; @import 'components/dropdown'; diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss new file mode 100644 index 00000000000..7e832b0f0b8 --- /dev/null +++ b/public/sass/components/_slate_editor.scss @@ -0,0 +1,146 @@ +.slate-query-field { + font-size: $font-size-root; + font-family: $font-family-monospace; + height: auto; +} + +.slate-query-field-wrapper { + position: relative; + display: inline-block; + padding: 6px 7px 4px; + width: 100%; + cursor: text; + line-height: $line-height-base; + color: $text-color-weak; + background-color: $panel-bg; + background-image: none; + border: $panel-border; + border-radius: $border-radius; + transition: all 0.3s; +} + +.slate-typeahead { + .typeahead { + position: absolute; + z-index: auto; + top: -10000px; + left: -10000px; + opacity: 0; + border-radius: $border-radius; + transition: opacity 0.75s; + border: $panel-border; + max-height: calc(66vh); + overflow-y: scroll; + max-width: calc(66%); + overflow-x: hidden; + outline: none; + list-style: none; + background: $panel-bg; + color: $text-color; + transition: opacity 0.4s ease-out; + box-shadow: $typeahead-shadow; + } + + .typeahead-group__title { + color: $text-color-weak; + font-size: $font-size-sm; + line-height: $line-height-base; + padding: $input-padding-y $input-padding-x; + } + + .typeahead-item { + height: auto; + font-family: $font-family-monospace; + padding: $input-padding-y $input-padding-x; + padding-left: $input-padding-x-lg; + font-size: $font-size-sm; + text-overflow: ellipsis; + overflow: hidden; + z-index: 1; + display: block; + white-space: nowrap; + cursor: pointer; + transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1); + } + + .typeahead-item__selected { + background-color: $typeahead-selected-bg; + color: $typeahead-selected-color; + } +} + +/* SYNTAX */ + +.slate-query-field { + .token.comment, + .token.block-comment, + .token.prolog, + .token.doctype, + .token.cdata { + color: $text-color-weak; + } + + .token.punctuation { + color: $text-color-weak; + } + + .token.property, + .token.tag, + .token.boolean, + .token.number, + .token.function-name, + .token.constant, + .token.symbol, + .token.deleted { + color: $query-red; + } + + .token.selector, + .token.attr-name, + .token.string, + .token.char, + .token.function, + .token.builtin, + .token.inserted { + color: $query-green; + } + + .token.operator, + .token.entity, + .token.url, + .token.variable { + color: $query-purple; + } + + .token.atrule, + .token.attr-value, + .token.keyword, + .token.class-name { + color: $query-blue; + } + + .token.regex, + .token.important { + color: $query-orange; + } + + .token.important { + font-weight: normal; + } + + .token.bold { + font-weight: bold; + } + .token.italic { + font-style: italic; + } + + .token.entity { + cursor: help; + } + + .namespace { + opacity: 0.7; + } +} diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 7dacccf6a87..876260c4f76 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -93,150 +93,3 @@ .query-row-tools { width: 4rem; } - -.query-field { - font-size: $font-size-root; - font-family: $font-family-monospace; - height: auto; -} - -.query-field-wrapper { - position: relative; - display: inline-block; - padding: 6px 7px 4px; - width: 100%; - cursor: text; - line-height: $line-height-base; - color: $text-color-weak; - background-color: $panel-bg; - background-image: none; - border: $panel-border; - border-radius: $border-radius; - transition: all 0.3s; -} - -.explore-typeahead { - .typeahead { - position: absolute; - z-index: auto; - top: -10000px; - left: -10000px; - opacity: 0; - border-radius: $border-radius; - transition: opacity 0.75s; - border: $panel-border; - max-height: calc(66vh); - overflow-y: scroll; - max-width: calc(66%); - overflow-x: hidden; - outline: none; - list-style: none; - background: $panel-bg; - color: $text-color; - transition: opacity 0.4s ease-out; - box-shadow: $typeahead-shadow; - } - - .typeahead-group__title { - color: $text-color-weak; - font-size: $font-size-sm; - line-height: $line-height-base; - padding: $input-padding-y $input-padding-x; - } - - .typeahead-item { - height: auto; - font-family: $font-family-monospace; - padding: $input-padding-y $input-padding-x; - padding-left: $input-padding-x-lg; - font-size: $font-size-sm; - text-overflow: ellipsis; - overflow: hidden; - z-index: 1; - display: block; - white-space: nowrap; - cursor: pointer; - transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), - background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1); - } - - .typeahead-item__selected { - background-color: $typeahead-selected-bg; - color: $typeahead-selected-color; - } -} - -/* SYNTAX */ - -.explore { - .token.comment, - .token.block-comment, - .token.prolog, - .token.doctype, - .token.cdata { - color: $text-color-weak; - } - - .token.punctuation { - color: $text-color-weak; - } - - .token.property, - .token.tag, - .token.boolean, - .token.number, - .token.function-name, - .token.constant, - .token.symbol, - .token.deleted { - color: $query-red; - } - - .token.selector, - .token.attr-name, - .token.string, - .token.char, - .token.function, - .token.builtin, - .token.inserted { - color: $query-green; - } - - .token.operator, - .token.entity, - .token.url, - .token.variable { - color: $query-purple; - } - - .token.atrule, - .token.attr-value, - .token.keyword, - .token.class-name { - color: $query-blue; - } - - .token.regex, - .token.important { - color: $query-orange; - } - - .token.important { - font-weight: normal; - } - - .token.bold { - font-weight: bold; - } - .token.italic { - font-style: italic; - } - - .token.entity { - cursor: help; - } - - .namespace { - opacity: 0.7; - } -} From 4113f7db470ae53f7ec5c502d877e74e9e15f383 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 12 Jun 2018 12:31:21 +0200 Subject: [PATCH 004/410] Trigger typeahead on Ctrl-Space --- public/app/containers/Explore/QueryField.tsx | 53 +++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index c0d51ad9cfc..8b0287e9b37 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -335,20 +335,30 @@ class QueryField extends React.Component { } onKeyDown = (event, change) => { - if (this.menuEl) { - const { typeaheadIndex, suggestions } = this.state; + const { typeaheadIndex, suggestions } = this.state; - switch (event.key) { - case 'Escape': { - if (this.menuEl) { - event.preventDefault(); - this.resetTypeahead(); - return true; - } - break; + switch (event.key) { + case 'Escape': { + if (this.menuEl) { + event.preventDefault(); + event.stopPropagation(); + this.resetTypeahead(); + return true; } + break; + } - case 'Tab': { + case ' ': { + if (event.ctrlKey) { + event.preventDefault(); + this.handleTypeahead(); + return true; + } + break; + } + + case 'Tab': { + if (this.menuEl) { // Dont blur input event.preventDefault(); if (!suggestions || suggestions.length === 0) { @@ -364,25 +374,30 @@ class QueryField extends React.Component { this.applyTypeahead(change, suggestion); return true; } + break; + } - case 'ArrowDown': { + case 'ArrowDown': { + if (this.menuEl) { // Select next suggestion event.preventDefault(); this.setState({ typeaheadIndex: typeaheadIndex + 1 }); - break; } + break; + } - case 'ArrowUp': { + case 'ArrowUp': { + if (this.menuEl) { // Select previous suggestion event.preventDefault(); this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) }); - break; } + break; + } - default: { - // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key); - break; - } + default: { + // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key); + break; } } return undefined; From 2ebda4bf4d6b9ac8b4983ad2ec6087dff7f50979 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 12 Jun 2018 14:53:09 +0200 Subject: [PATCH 005/410] Make suggestions an object --- public/app/containers/Explore/QueryField.tsx | 4 +++- public/app/containers/Explore/Typeahead.tsx | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index 8b0287e9b37..f241e9f062a 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -545,7 +545,9 @@ class QueryField extends React.Component { let selectedIndex = Math.max(this.state.typeaheadIndex, 0); const flattenedSuggestions = flattenSuggestions(suggestions); selectedIndex = selectedIndex % flattenedSuggestions.length || 0; - const selectedKeys = flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []; + const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map( + i => (typeof i === 'object' ? i.text : i) + ); // Create typeahead in DOM root so we can later position it absolutely return ( diff --git a/public/app/containers/Explore/Typeahead.tsx b/public/app/containers/Explore/Typeahead.tsx index 4943622fe4e..e7f0a9a0dce 100644 --- a/public/app/containers/Explore/Typeahead.tsx +++ b/public/app/containers/Explore/Typeahead.tsx @@ -41,9 +41,17 @@ class TypeaheadGroup extends React.PureComponent {
  • {label}
      - {items.map(item => ( - -1} label={item} /> - ))} + {items.map(item => { + const text = typeof item === 'object' ? item.text : item; + return ( + -1} + label={text} + /> + ); + })}
  • ); From 73ddf2c3ed5d119ea97463871256a60714300dc2 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 12 Jun 2018 17:12:03 +0200 Subject: [PATCH 006/410] hint support for typeahead --- public/app/containers/Explore/Typeahead.tsx | 7 +++++-- public/sass/components/_slate_editor.scss | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/public/app/containers/Explore/Typeahead.tsx b/public/app/containers/Explore/Typeahead.tsx index e7f0a9a0dce..44fce7f8c7e 100644 --- a/public/app/containers/Explore/Typeahead.tsx +++ b/public/app/containers/Explore/Typeahead.tsx @@ -23,12 +23,13 @@ class TypeaheadItem extends React.PureComponent { }; render() { - const { isSelected, label, onClickItem } = this.props; + const { hint, isSelected, label, onClickItem } = this.props; const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item'; const onClick = () => onClickItem(label); return (
  • {label} + {hint && isSelected ?
    {hint}
    : null}
  • ); } @@ -43,12 +44,14 @@ class TypeaheadGroup extends React.PureComponent {
      {items.map(item => { const text = typeof item === 'object' ? item.text : item; + const label = typeof item === 'object' ? item.display || item.text : item; return ( -1} - label={text} + hint={item.hint} + label={label} /> ); })} diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss index 7e832b0f0b8..de8a6e6d721 100644 --- a/public/sass/components/_slate_editor.scss +++ b/public/sass/components/_slate_editor.scss @@ -67,6 +67,11 @@ .typeahead-item__selected { background-color: $typeahead-selected-bg; color: $typeahead-selected-color; + + .typeahead-item-hint { + font-size: $font-size-xs; + color: $text-color; + } } } From b2f497b100b7b94a07a22d594b26cf14e89273ae Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 12 Jun 2018 19:02:02 +0200 Subject: [PATCH 007/410] batch DOM reads from query field typeahead --- public/app/containers/Explore/QueryField.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index f241e9f062a..bbfbb067d0b 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -522,10 +522,17 @@ class QueryField extends React.Component { // Align menu overlay to editor node if (node) { + // Read from DOM const rect = node.parentElement.getBoundingClientRect(); - menu.style.opacity = 1; - menu.style.top = `${rect.top + window.scrollY + rect.height + 4}px`; - menu.style.left = `${rect.left + window.scrollX - 2}px`; + const scrollX = window.scrollX; + const scrollY = window.scrollY; + + // Write DOM + requestAnimationFrame(() => { + menu.style.opacity = 1; + menu.style.top = `${rect.top + scrollY + rect.height + 4}px`; + menu.style.left = `${rect.left + scrollX - 2}px`; + }); } }; From bbe6ab509639c6f3ddffe0715e95ce52aad3fb4c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 14 Jun 2018 10:31:09 +0100 Subject: [PATCH 008/410] Fix Queryfield metrics field missing --- 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 bbfbb067d0b..bedb955b9b9 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -135,7 +135,7 @@ class QueryField extends React.Component { if (!this.state.metrics) { return; } - setPrismTokens(this.props.language, 'metrics', this.state.metrics); + setPrismTokens(this.props.prismLanguage, 'metrics', this.state.metrics); // Trigger re-render window.requestAnimationFrame(() => { From a9e1e5f346cf23f1ae6a137e3560e72d61f278fb Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 14 Jun 2018 11:41:01 +0100 Subject: [PATCH 009/410] Fix queryfield wrapper css --- public/app/containers/Explore/QueryRows.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index 24079ac0f69..a968e1e2c64 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -56,7 +56,7 @@ class QueryRow extends PureComponent {
    -
    +
    Date: Fri, 15 Jun 2018 10:11:32 -0300 Subject: [PATCH 010/410] 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 3bbe39c5ad92102412229717c34214c1af401354 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 3 Jul 2018 12:45:04 +0200 Subject: [PATCH 011/410] Karma to Jest: postgres datasource --- ...datasource_specs.ts => datasource.jest.ts} | 104 ++++++++---------- 1 file changed, 45 insertions(+), 59 deletions(-) rename public/app/plugins/datasource/postgres/specs/{datasource_specs.ts => datasource.jest.ts} (66%) diff --git a/public/app/plugins/datasource/postgres/specs/datasource_specs.ts b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts similarity index 66% rename from public/app/plugins/datasource/postgres/specs/datasource_specs.ts rename to public/app/plugins/datasource/postgres/specs/datasource.jest.ts index f17ad019f26..d00e036a514 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts @@ -1,28 +1,17 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import moment from 'moment'; -import helpers from 'test/specs/helpers'; import { PostgresDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; +import { TemplateSrvStub } from 'test/specs/helpers'; describe('PostgreSQLDatasource', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { name: 'postgresql' }; + let instanceSettings = { name: 'postgresql' }; - 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(PostgresDatasource, { - instanceSettings: instanceSettings, - }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); + let backendSrv = {}; + let templateSrv = new TemplateSrvStub(); + let ctx = { + ds: new PostgresDatasource(instanceSettings, backendSrv, {}, templateSrv), + backendSrv + }; describe('When performing annotationQuery', function() { let results; @@ -59,26 +48,25 @@ describe('PostgreSQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({ data: response, status: 200 }); - }; + ctx.backendSrv.datasourceRequest = jest.fn((options) => { + return Promise.resolve({ data: response, status: 200 }); + }); ctx.ds.annotationQuery(options).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return annotation list', function() { - expect(results.length).to.be(3); + expect(results.length).toBe(3); - expect(results[0].text).to.be('some text'); - expect(results[0].tags[0]).to.be('TagA'); - expect(results[0].tags[1]).to.be('TagB'); + expect(results[0].text).toBe('some text'); + expect(results[0].tags[0]).toBe('TagA'); + expect(results[0].tags[1]).toBe('TagB'); - expect(results[1].tags[0]).to.be('TagB'); - expect(results[1].tags[1]).to.be('TagC'); + expect(results[1].tags[0]).toBe('TagB'); + expect(results[1].tags[1]).toBe('TagC'); - expect(results[2].tags.length).to.be(0); + expect(results[2].tags.length).toBe(0); }); }); @@ -103,19 +91,18 @@ describe('PostgreSQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({ data: response, status: 200 }); - }; + ctx.backendSrv.datasourceRequest = jest.fn((options) => { + return Promise.resolve({ data: response, status: 200 }); + }); ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of all column values', function() { - expect(results.length).to.be(6); - expect(results[0].text).to.be('aTitle'); - expect(results[5].text).to.be('some text3'); + expect(results.length).toBe(6); + expect(results[0].text).toBe('aTitle'); + expect(results[5].text).toBe('some text3'); }); }); @@ -140,21 +127,20 @@ describe('PostgreSQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({ data: response, status: 200 }); - }; + ctx.backendSrv.datasourceRequest = jest.fn((options) => { + return Promise.resolve({ data: response, status: 200 }); + }); ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of as text, value', function() { - expect(results.length).to.be(3); - expect(results[0].text).to.be('aTitle'); - expect(results[0].value).to.be('value1'); - expect(results[2].text).to.be('aTitle3'); - expect(results[2].value).to.be('value3'); + expect(results.length).toBe(3); + expect(results[0].text).toBe('aTitle'); + expect(results[0].value).toBe('value1'); + expect(results[2].text).toBe('aTitle3'); + expect(results[2].value).toBe('value3'); }); }); @@ -178,20 +164,20 @@ describe('PostgreSQLDatasource', function() { }, }; - beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({ data: response, status: 200 }); - }; + beforeEach(() => { + ctx.backendSrv.datasourceRequest = jest.fn((options) => { + return Promise.resolve({ data: response, status: 200 }); + }); ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); + //ctx.$rootScope.$apply(); }); it('should return list of unique keys', function() { - expect(results.length).to.be(1); - expect(results[0].text).to.be('aTitle'); - expect(results[0].value).to.be('same'); + expect(results.length).toBe(1); + expect(results[0].text).toBe('aTitle'); + expect(results[0].value).toBe('same'); }); }); @@ -202,33 +188,33 @@ describe('PostgreSQLDatasource', function() { describe('and value is a string', () => { it('should return an unquoted value', () => { - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc'); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc'); }); }); describe('and value is a number', () => { it('should return an unquoted value', () => { - expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000); + expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000); }); }); describe('and value is an array of strings', () => { it('should return comma separated quoted values', () => { - expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'"); + expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'"); }); }); describe('and variable allows multi-value and is a string', () => { it('should return a quoted value', () => { ctx.variable.multi = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); }); }); describe('and variable allows all and is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); }); }); }); From d4bd52139aaba4091d90c237d199f185b0992164 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 3 Jul 2018 12:56:36 +0200 Subject: [PATCH 012/410] Karma to Jest: MySQL datasource --- ...datasource_specs.ts => datasource.jest.ts} | 102 ++++++++---------- 1 file changed, 44 insertions(+), 58 deletions(-) rename public/app/plugins/datasource/mysql/specs/{datasource_specs.ts => datasource.jest.ts} (66%) diff --git a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts similarity index 66% rename from public/app/plugins/datasource/mysql/specs/datasource_specs.ts rename to public/app/plugins/datasource/mysql/specs/datasource.jest.ts index 0373797f211..5a5881b3165 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts @@ -1,28 +1,18 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; +//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import moment from 'moment'; -import helpers from 'test/specs/helpers'; +import { TemplateSrvStub } from 'test/specs/helpers'; import { MysqlDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; describe('MySQLDatasource', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { name: 'mysql' }; - 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(MysqlDatasource, { - instanceSettings: instanceSettings, - }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); + let instanceSettings = { name: 'mysql' }; + let backendSrv = {}; + let templateSrv = new TemplateSrvStub(); + let ctx = { + ds: new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv), + backendSrv + }; describe('When performing annotationQuery', function() { let results; @@ -59,26 +49,25 @@ describe('MySQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({ data: response, status: 200 }); - }; + ctx.backendSrv.datasourceRequest = jest.fn((options) => { + return Promise.resolve({ data: response, status: 200 }); + }); ctx.ds.annotationQuery(options).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return annotation list', function() { - expect(results.length).to.be(3); + expect(results.length).toBe(3); - expect(results[0].text).to.be('some text'); - expect(results[0].tags[0]).to.be('TagA'); - expect(results[0].tags[1]).to.be('TagB'); + expect(results[0].text).toBe('some text'); + expect(results[0].tags[0]).toBe('TagA'); + expect(results[0].tags[1]).toBe('TagB'); - expect(results[1].tags[0]).to.be('TagB'); - expect(results[1].tags[1]).to.be('TagC'); + expect(results[1].tags[0]).toBe('TagB'); + expect(results[1].tags[1]).toBe('TagC'); - expect(results[2].tags.length).to.be(0); + expect(results[2].tags.length).toBe(0); }); }); @@ -103,19 +92,18 @@ describe('MySQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({ data: response, status: 200 }); - }; + ctx.backendSrv.datasourceRequest = jest.fn((options) => { + return Promise.resolve({ data: response, status: 200 }); + }); ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of all column values', function() { - expect(results.length).to.be(6); - expect(results[0].text).to.be('aTitle'); - expect(results[5].text).to.be('some text3'); + expect(results.length).toBe(6); + expect(results[0].text).toBe('aTitle'); + expect(results[5].text).toBe('some text3'); }); }); @@ -140,21 +128,20 @@ describe('MySQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({ data: response, status: 200 }); - }; + ctx.backendSrv.datasourceRequest = jest.fn((options) => { + return Promise.resolve({ data: response, status: 200 }); + }); ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of as text, value', function() { - expect(results.length).to.be(3); - expect(results[0].text).to.be('aTitle'); - expect(results[0].value).to.be('value1'); - expect(results[2].text).to.be('aTitle3'); - expect(results[2].value).to.be('value3'); + expect(results.length).toBe(3); + expect(results[0].text).toBe('aTitle'); + expect(results[0].value).toBe('value1'); + expect(results[2].text).toBe('aTitle3'); + expect(results[2].value).toBe('value3'); }); }); @@ -179,19 +166,18 @@ describe('MySQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({ data: response, status: 200 }); - }; + ctx.backendSrv.datasourceRequest = jest.fn((options) => { + return Promise.resolve({ data: response, status: 200 }); + }); ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of unique keys', function() { - expect(results.length).to.be(1); - expect(results[0].text).to.be('aTitle'); - expect(results[0].value).to.be('same'); + expect(results.length).toBe(1); + expect(results[0].text).toBe('aTitle'); + expect(results[0].value).toBe('same'); }); }); @@ -202,33 +188,33 @@ describe('MySQLDatasource', function() { describe('and value is a string', () => { it('should return an unquoted value', () => { - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc'); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc'); }); }); describe('and value is a number', () => { it('should return an unquoted value', () => { - expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000); + expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000); }); }); describe('and value is an array of strings', () => { it('should return comma separated quoted values', () => { - expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'"); + expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'"); }); }); describe('and variable allows multi-value and value is a string', () => { it('should return a quoted value', () => { ctx.variable.multi = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); }); }); describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); }); }); }); From 42ba0dc73f3037aaa51f63ced03fc6eb0fa02452 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 3 Jul 2018 13:20:36 +0200 Subject: [PATCH 013/410] Karma to Jest: Cloudwatch datasource --- ...datasource_specs.ts => datasource.jest.ts} | 160 ++++++++++-------- public/test/specs/helpers.ts | 18 ++ 2 files changed, 103 insertions(+), 75 deletions(-) rename public/app/plugins/datasource/cloudwatch/specs/{datasource_specs.ts => datasource.jest.ts} (74%) diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts similarity index 74% rename from public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts rename to public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts index b2c4950d3ce..705c35f2ebc 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts @@ -1,32 +1,42 @@ import '../datasource'; -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; -import helpers from 'test/specs/helpers'; +//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; +import { TemplateSrvStub, jestTimeSrvStub } from 'test/specs/helpers'; import CloudWatchDatasource from '../datasource'; import 'app/features/dashboard/time_srv'; describe('CloudWatchDatasource', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { + + let instanceSettings = { jsonData: { defaultRegion: 'us-east-1', access: 'proxy' }, }; + let templateSrv = new TemplateSrvStub(); + let timeSrv = new jestTimeSrvStub(); + let backendSrv = {}; + let ctx = { + backendSrv, + templateSrv + }; - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach(angularMocks.module('grafana.controllers')); - beforeEach(ctx.providePhase(['templateSrv', 'backendSrv'])); - beforeEach(ctx.createService('timeSrv')); + beforeEach(() => { + ctx.ds = new CloudWatchDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv); + }); + // beforeEach(angularMocks.module('grafana.core')); + // beforeEach(angularMocks.module('grafana.services')); + // beforeEach(angularMocks.module('grafana.controllers')); + // beforeEach(ctx.providePhase(['templateSrv', 'backendSrv'])); + // beforeEach(ctx.createService('timeSrv')); - beforeEach( - angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(CloudWatchDatasource, { - instanceSettings: instanceSettings, - }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); + // beforeEach( + // angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { + // ctx.$q = $q; + // ctx.$httpBackend = $httpBackend; + // ctx.$rootScope = $rootScope; + // ctx.ds = $injector.instantiate(CloudWatchDatasource, { + // instanceSettings: instanceSettings, + // }); + // $httpBackend.when('GET', /\.html$/).respond(''); + // }) + // ); describe('When performing CloudWatch query', function() { var requestParams; @@ -67,24 +77,24 @@ describe('CloudWatchDatasource', function() { }, }; - beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(params) { + beforeEach(async () => { + ctx.backendSrv.datasourceRequest = await jest.fn((params) => { requestParams = params.data; - return ctx.$q.when({ data: response }); - }; + return Promise.resolve({ data: response }); + }); }); it('should generate the correct query', function(done) { ctx.ds.query(query).then(function() { var params = requestParams.queries[0]; - expect(params.namespace).to.be(query.targets[0].namespace); - expect(params.metricName).to.be(query.targets[0].metricName); - expect(params.dimensions['InstanceId']).to.be('i-12345678'); - expect(params.statistics).to.eql(query.targets[0].statistics); - expect(params.period).to.be(query.targets[0].period); + expect(params.namespace).toBe(query.targets[0].namespace); + expect(params.metricName).toBe(query.targets[0].metricName); + expect(params.dimensions['InstanceId']).toBe('i-12345678'); + expect(params.statistics).toEqual(query.targets[0].statistics); + expect(params.period).toBe(query.targets[0].period); done(); }); - ctx.$rootScope.$apply(); + //ctx.$rootScope.$apply(); }); it('should generate the correct query with interval variable', function(done) { @@ -111,19 +121,19 @@ describe('CloudWatchDatasource', function() { ctx.ds.query(query).then(function() { var params = requestParams.queries[0]; - expect(params.period).to.be('600'); + expect(params.period).toBe('600'); done(); }); - ctx.$rootScope.$apply(); + //ctx.$rootScope.$apply(); }); it('should return series list', function(done) { ctx.ds.query(query).then(function(result) { - expect(result.data[0].target).to.be(response.results.A.series[0].name); - expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]); + expect(result.data[0].target).toBe(response.results.A.series[0].name); + expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]); done(); }); - ctx.$rootScope.$apply(); + //ctx.$rootScope.$apply(); }); it('should generate the correct targets by expanding template variables', function() { @@ -173,7 +183,7 @@ describe('CloudWatchDatasource', function() { ]; var result = ctx.ds.expandTemplateVariable(targets, {}, templateSrv); - expect(result[0].dimensions.InstanceId).to.be('i-34567890'); + expect(result[0].dimensions.InstanceId).toBe('i-34567890'); }); it('should generate the correct targets by expanding template variables from url', function() { @@ -220,7 +230,7 @@ describe('CloudWatchDatasource', function() { ]; var result = ctx.ds.expandTemplateVariable(targets, {}, templateSrv); - expect(result[0].dimensions.InstanceId).to.be('i-45678901'); + expect(result[0].dimensions.InstanceId).toBe('i-45678901'); }); }); @@ -228,21 +238,21 @@ describe('CloudWatchDatasource', function() { it('should return the datasource region if empty or "default"', function() { var defaultRegion = instanceSettings.jsonData.defaultRegion; - expect(ctx.ds.getActualRegion()).to.be(defaultRegion); - expect(ctx.ds.getActualRegion('')).to.be(defaultRegion); - expect(ctx.ds.getActualRegion('default')).to.be(defaultRegion); + expect(ctx.ds.getActualRegion()).toBe(defaultRegion); + expect(ctx.ds.getActualRegion('')).toBe(defaultRegion); + expect(ctx.ds.getActualRegion('default')).toBe(defaultRegion); }); it('should return the specified region if specified', function() { - expect(ctx.ds.getActualRegion('some-fake-region-1')).to.be('some-fake-region-1'); + expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1'); }); var requestParams; beforeEach(function() { - ctx.ds.performTimeSeriesQuery = function(request) { + ctx.ds.performTimeSeriesQuery = jest.fn((request) => { requestParams = request; - return ctx.$q.when({ data: {} }); - }; + return Promise.resolve({ data: {} }); + }); }); it('should query for the datasource region if empty or "default"', function(done) { @@ -264,10 +274,10 @@ describe('CloudWatchDatasource', function() { }; ctx.ds.query(query).then(function(result) { - expect(requestParams.queries[0].region).to.be(instanceSettings.jsonData.defaultRegion); + expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion); done(); }); - ctx.$rootScope.$apply(); + //ctx.$rootScope.$apply(); }); }); @@ -311,18 +321,18 @@ describe('CloudWatchDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(params) { - return ctx.$q.when({ data: response }); - }; + ctx.backendSrv.datasourceRequest = jest.fn((params) => { + return Promise.resolve({ data: response }); + }); }); it('should return series list', function(done) { ctx.ds.query(query).then(function(result) { - expect(result.data[0].target).to.be(response.results.A.series[0].name); - expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]); + expect(result.data[0].target).toBe(response.results.A.series[0].name); + expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]); done(); }); - ctx.$rootScope.$apply(); + //ctx.$rootScope.$apply(); }); }); @@ -332,14 +342,14 @@ describe('CloudWatchDatasource', function() { scenario.setup = setupCallback => { beforeEach(() => { setupCallback(); - ctx.backendSrv.datasourceRequest = args => { + ctx.backendSrv.datasourceRequest = jest.fn((args) => { scenario.request = args.data; - return ctx.$q.when({ data: scenario.requestResponse }); - }; + return Promise.resolve({ data: scenario.requestResponse }); + }); ctx.ds.metricFindQuery(query).then(args => { scenario.result = args; }); - ctx.$rootScope.$apply(); + //ctx.$rootScope.$apply(); }); }; @@ -359,9 +369,9 @@ describe('CloudWatchDatasource', function() { }); it('should call __GetRegions and return result', () => { - expect(scenario.result[0].text).to.contain('us-east-1'); - expect(scenario.request.queries[0].type).to.be('metricFindQuery'); - expect(scenario.request.queries[0].subtype).to.be('regions'); + expect(scenario.result[0].text).toContain('us-east-1'); + expect(scenario.request.queries[0].type).toBe('metricFindQuery'); + expect(scenario.request.queries[0].subtype).toBe('regions'); }); }); @@ -377,9 +387,9 @@ describe('CloudWatchDatasource', function() { }); it('should call __GetNamespaces and return result', () => { - expect(scenario.result[0].text).to.contain('AWS/EC2'); - expect(scenario.request.queries[0].type).to.be('metricFindQuery'); - expect(scenario.request.queries[0].subtype).to.be('namespaces'); + expect(scenario.result[0].text).toContain('AWS/EC2'); + expect(scenario.request.queries[0].type).toBe('metricFindQuery'); + expect(scenario.request.queries[0].subtype).toBe('namespaces'); }); }); @@ -395,9 +405,9 @@ describe('CloudWatchDatasource', function() { }); it('should call __GetMetrics and return result', () => { - expect(scenario.result[0].text).to.be('CPUUtilization'); - expect(scenario.request.queries[0].type).to.be('metricFindQuery'); - expect(scenario.request.queries[0].subtype).to.be('metrics'); + expect(scenario.result[0].text).toBe('CPUUtilization'); + expect(scenario.request.queries[0].type).toBe('metricFindQuery'); + expect(scenario.request.queries[0].subtype).toBe('metrics'); }); }); @@ -413,9 +423,9 @@ describe('CloudWatchDatasource', function() { }); it('should call __GetDimensions and return result', () => { - expect(scenario.result[0].text).to.be('InstanceId'); - expect(scenario.request.queries[0].type).to.be('metricFindQuery'); - expect(scenario.request.queries[0].subtype).to.be('dimension_keys'); + expect(scenario.result[0].text).toBe('InstanceId'); + expect(scenario.request.queries[0].type).toBe('metricFindQuery'); + expect(scenario.request.queries[0].subtype).toBe('dimension_keys'); }); }); @@ -431,9 +441,9 @@ describe('CloudWatchDatasource', function() { }); it('should call __ListMetrics and return result', () => { - expect(scenario.result[0].text).to.contain('i-12345678'); - expect(scenario.request.queries[0].type).to.be('metricFindQuery'); - expect(scenario.request.queries[0].subtype).to.be('dimension_values'); + expect(scenario.result[0].text).toContain('i-12345678'); + expect(scenario.request.queries[0].type).toBe('metricFindQuery'); + expect(scenario.request.queries[0].subtype).toBe('dimension_values'); }); }); @@ -449,9 +459,9 @@ describe('CloudWatchDatasource', function() { }); it('should call __ListMetrics and return result', () => { - expect(scenario.result[0].text).to.contain('i-12345678'); - expect(scenario.request.queries[0].type).to.be('metricFindQuery'); - expect(scenario.request.queries[0].subtype).to.be('dimension_values'); + expect(scenario.result[0].text).toContain('i-12345678'); + expect(scenario.request.queries[0].type).toBe('metricFindQuery'); + expect(scenario.request.queries[0].subtype).toBe('dimension_values'); }); }); @@ -544,7 +554,7 @@ describe('CloudWatchDatasource', function() { let now = new Date(options.range.from.valueOf() + t[2] * 1000); let expected = t[3]; let actual = ctx.ds.getPeriod(target, options, now); - expect(actual).to.be(expected); + expect(actual).toBe(expected); } }); }); diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 677419f3f75..811587b2c91 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -195,6 +195,24 @@ export function TemplateSrvStub() { }; } +export function jestTimeSrvStub() { + this.init = jest.fn(); + this.time = { from: 'now-1h', to: 'now' }; + this.timeRange = jest.fn(parse => { + if (parse === false) { + return this.time; + } + return { + from: dateMath.parse(this.time.from, false), + to: dateMath.parse(this.time.to, true), + }; + }); + this.replace = jest.fn(target => target); + this.setTime = jest.fn(time => { + this.time = time; + }); +} + var allDeps = { ContextSrvStub, TemplateSrvStub, From a37a0c7be3c05ba325384751153959b3dfe3381f Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 3 Jul 2018 13:21:59 +0200 Subject: [PATCH 014/410] Remove comments --- .../cloudwatch/specs/datasource.jest.ts | 34 +++---------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts index 705c35f2ebc..4e479e6a83e 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts @@ -5,7 +5,6 @@ import CloudWatchDatasource from '../datasource'; import 'app/features/dashboard/time_srv'; describe('CloudWatchDatasource', function() { - let instanceSettings = { jsonData: { defaultRegion: 'us-east-1', access: 'proxy' }, }; @@ -14,29 +13,12 @@ describe('CloudWatchDatasource', function() { let backendSrv = {}; let ctx = { backendSrv, - templateSrv + templateSrv, }; beforeEach(() => { ctx.ds = new CloudWatchDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv); }); - // beforeEach(angularMocks.module('grafana.core')); - // beforeEach(angularMocks.module('grafana.services')); - // beforeEach(angularMocks.module('grafana.controllers')); - // beforeEach(ctx.providePhase(['templateSrv', 'backendSrv'])); - // beforeEach(ctx.createService('timeSrv')); - - // beforeEach( - // angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - // ctx.$q = $q; - // ctx.$httpBackend = $httpBackend; - // ctx.$rootScope = $rootScope; - // ctx.ds = $injector.instantiate(CloudWatchDatasource, { - // instanceSettings: instanceSettings, - // }); - // $httpBackend.when('GET', /\.html$/).respond(''); - // }) - // ); describe('When performing CloudWatch query', function() { var requestParams; @@ -78,7 +60,7 @@ describe('CloudWatchDatasource', function() { }; beforeEach(async () => { - ctx.backendSrv.datasourceRequest = await jest.fn((params) => { + ctx.backendSrv.datasourceRequest = await jest.fn(params => { requestParams = params.data; return Promise.resolve({ data: response }); }); @@ -94,7 +76,6 @@ describe('CloudWatchDatasource', function() { expect(params.period).toBe(query.targets[0].period); done(); }); - //ctx.$rootScope.$apply(); }); it('should generate the correct query with interval variable', function(done) { @@ -124,7 +105,6 @@ describe('CloudWatchDatasource', function() { expect(params.period).toBe('600'); done(); }); - //ctx.$rootScope.$apply(); }); it('should return series list', function(done) { @@ -133,7 +113,6 @@ describe('CloudWatchDatasource', function() { expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]); done(); }); - //ctx.$rootScope.$apply(); }); it('should generate the correct targets by expanding template variables', function() { @@ -249,7 +228,7 @@ describe('CloudWatchDatasource', function() { var requestParams; beforeEach(function() { - ctx.ds.performTimeSeriesQuery = jest.fn((request) => { + ctx.ds.performTimeSeriesQuery = jest.fn(request => { requestParams = request; return Promise.resolve({ data: {} }); }); @@ -277,7 +256,6 @@ describe('CloudWatchDatasource', function() { expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion); done(); }); - //ctx.$rootScope.$apply(); }); }); @@ -321,7 +299,7 @@ describe('CloudWatchDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = jest.fn((params) => { + ctx.backendSrv.datasourceRequest = jest.fn(params => { return Promise.resolve({ data: response }); }); }); @@ -332,7 +310,6 @@ describe('CloudWatchDatasource', function() { expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]); done(); }); - //ctx.$rootScope.$apply(); }); }); @@ -342,14 +319,13 @@ describe('CloudWatchDatasource', function() { scenario.setup = setupCallback => { beforeEach(() => { setupCallback(); - ctx.backendSrv.datasourceRequest = jest.fn((args) => { + ctx.backendSrv.datasourceRequest = jest.fn(args => { scenario.request = args.data; return Promise.resolve({ data: scenario.requestResponse }); }); ctx.ds.metricFindQuery(query).then(args => { scenario.result = args; }); - //ctx.$rootScope.$apply(); }); }; From 962340bcd1adce74412cf57e04e920efa761b00f Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 3 Jul 2018 13:26:42 +0200 Subject: [PATCH 015/410] Create new instance in beforeEach --- .../datasource/mysql/specs/datasource.jest.ts | 16 +++++++++------- .../datasource/postgres/specs/datasource.jest.ts | 15 +++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts index 5a5881b3165..5a97e4b441d 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts @@ -5,15 +5,17 @@ import { MysqlDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; describe('MySQLDatasource', function() { - let instanceSettings = { name: 'mysql' }; let backendSrv = {}; let templateSrv = new TemplateSrvStub(); let ctx = { - ds: new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv), - backendSrv + backendSrv, }; + beforeEach(() => { + ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv); + }); + describe('When performing annotationQuery', function() { let results; @@ -49,7 +51,7 @@ describe('MySQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = jest.fn((options) => { + ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); ctx.ds.annotationQuery(options).then(function(data) { @@ -92,7 +94,7 @@ describe('MySQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = jest.fn((options) => { + ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); ctx.ds.metricFindQuery(query).then(function(data) { @@ -128,7 +130,7 @@ describe('MySQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = jest.fn((options) => { + ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); ctx.ds.metricFindQuery(query).then(function(data) { @@ -166,7 +168,7 @@ describe('MySQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = jest.fn((options) => { + ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); ctx.ds.metricFindQuery(query).then(function(data) { diff --git a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts index d00e036a514..87e4c348379 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts @@ -9,10 +9,13 @@ describe('PostgreSQLDatasource', function() { let backendSrv = {}; let templateSrv = new TemplateSrvStub(); let ctx = { - ds: new PostgresDatasource(instanceSettings, backendSrv, {}, templateSrv), - backendSrv + backendSrv, }; + beforeEach(() => { + ctx.ds = new PostgresDatasource(instanceSettings, backendSrv, {}, templateSrv); + }); + describe('When performing annotationQuery', function() { let results; @@ -48,7 +51,7 @@ describe('PostgreSQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = jest.fn((options) => { + ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); ctx.ds.annotationQuery(options).then(function(data) { @@ -91,7 +94,7 @@ describe('PostgreSQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = jest.fn((options) => { + ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); ctx.ds.metricFindQuery(query).then(function(data) { @@ -127,7 +130,7 @@ describe('PostgreSQLDatasource', function() { }; beforeEach(function() { - ctx.backendSrv.datasourceRequest = jest.fn((options) => { + ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); ctx.ds.metricFindQuery(query).then(function(data) { @@ -165,7 +168,7 @@ describe('PostgreSQLDatasource', function() { }; beforeEach(() => { - ctx.backendSrv.datasourceRequest = jest.fn((options) => { + ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ data: response, status: 200 }); }); ctx.ds.metricFindQuery(query).then(function(data) { From 74bf80962d395ce2280434bf52ea4a44626f934c Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 4 Jul 2018 11:16:31 +0200 Subject: [PATCH 016/410] Add mock to test files --- .../cloudwatch/specs/datasource.jest.ts | 15 ++++++++++++--- .../datasource/mysql/specs/datasource.jest.ts | 7 ++++--- .../postgres/specs/datasource.jest.ts | 5 +++-- public/test/specs/helpers.ts | 18 ------------------ 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts index 4e479e6a83e..fbeaaad8e67 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts @@ -1,15 +1,24 @@ import '../datasource'; -//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; -import { TemplateSrvStub, jestTimeSrvStub } from 'test/specs/helpers'; +import { TemplateSrvStub } from 'test/specs/helpers'; import CloudWatchDatasource from '../datasource'; import 'app/features/dashboard/time_srv'; +import * as dateMath from 'app/core/utils/datemath'; describe('CloudWatchDatasource', function() { let instanceSettings = { jsonData: { defaultRegion: 'us-east-1', access: 'proxy' }, }; let templateSrv = new TemplateSrvStub(); - let timeSrv = new jestTimeSrvStub(); + + let timeSrv = { + time: { from: 'now-1h', to: 'now' }, + timeRange: jest.fn(() => { + return { + from: dateMath.parse(timeSrv.time.from, false), + to: dateMath.parse(timeSrv.time.to, true), + }; + }), + }; let backendSrv = {}; let ctx = { backendSrv, diff --git a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts index 5a97e4b441d..be33f5f8858 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.jest.ts @@ -1,13 +1,14 @@ -//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import moment from 'moment'; -import { TemplateSrvStub } from 'test/specs/helpers'; import { MysqlDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; describe('MySQLDatasource', function() { let instanceSettings = { name: 'mysql' }; let backendSrv = {}; - let templateSrv = new TemplateSrvStub(); + let templateSrv = { + replace: jest.fn(text => text), + }; + let ctx = { backendSrv, }; diff --git a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts index 87e4c348379..107cd76e6c5 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.jest.ts @@ -1,13 +1,14 @@ import moment from 'moment'; import { PostgresDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; -import { TemplateSrvStub } from 'test/specs/helpers'; describe('PostgreSQLDatasource', function() { let instanceSettings = { name: 'postgresql' }; let backendSrv = {}; - let templateSrv = new TemplateSrvStub(); + let templateSrv = { + replace: jest.fn(text => text), + }; let ctx = { backendSrv, }; diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 811587b2c91..677419f3f75 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -195,24 +195,6 @@ export function TemplateSrvStub() { }; } -export function jestTimeSrvStub() { - this.init = jest.fn(); - this.time = { from: 'now-1h', to: 'now' }; - this.timeRange = jest.fn(parse => { - if (parse === false) { - return this.time; - } - return { - from: dateMath.parse(this.time.from, false), - to: dateMath.parse(this.time.to, true), - }; - }); - this.replace = jest.fn(target => target); - this.setTime = jest.fn(time => { - this.time = time; - }); -} - var allDeps = { ContextSrvStub, TemplateSrvStub, From 664944980a86c7082ce20e007a8789e949543453 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Mon, 16 Apr 2018 15:27:25 +0900 Subject: [PATCH 017/410] 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 Date: Mon, 16 Apr 2018 15:32:39 +0900 Subject: [PATCH 018/410] 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 Date: Mon, 16 Apr 2018 16:50:13 +0900 Subject: [PATCH 019/410] 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 @@ -
    +
    @@ -20,7 +20,7 @@
    -
    +
    @@ -31,18 +31,31 @@
    -
    +
    - - + +
    -
    - - - +
    + + +
    +
    + +
    +
    + + +
    +
    + + + Alias replacement variables:
    • {{metric}}
    • @@ -54,12 +67,12 @@
    -
    - +
    +
    -
    -
    +
    +
    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 Date: Fri, 22 Jun 2018 16:25:04 +0900 Subject: [PATCH 020/410] 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 Date: Fri, 22 Jun 2018 16:35:17 +0900 Subject: [PATCH 021/410] 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 6670acd0825893d937006e81cb378cbeee6da9c2 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 9 Jul 2018 12:52:56 +0200 Subject: [PATCH 022/410] Remove irrelevant tests and templateSrv stub --- .../datasource/cloudwatch/datasource.ts | 1 + .../cloudwatch/specs/datasource.jest.ts | 110 ++---------------- 2 files changed, 10 insertions(+), 101 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 4101759ec1d..391f65bd7ae 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -404,6 +404,7 @@ export default class CloudWatchDatasource { } expandTemplateVariable(targets, scopedVars, templateSrv) { + // Datasource and template srv logic uber-complected. This should be cleaned up. return _.chain(targets) .map(target => { var dimensionKey = _.findKey(target.dimensions, v => { diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts index fbeaaad8e67..b06d00b3ee4 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts @@ -1,14 +1,19 @@ import '../datasource'; -import { TemplateSrvStub } from 'test/specs/helpers'; import CloudWatchDatasource from '../datasource'; import 'app/features/dashboard/time_srv'; import * as dateMath from 'app/core/utils/datemath'; +import _ from 'lodash'; describe('CloudWatchDatasource', function() { let instanceSettings = { jsonData: { defaultRegion: 'us-east-1', access: 'proxy' }, }; - let templateSrv = new TemplateSrvStub(); + + let templateSrv = { + templateSettings: { interpolate: /\[\[([\s\S]+?)\]\]/g }, + replace: jest.fn(text => _.template(text, templateSrv.templateSettings)(templateSrv.data)), + variableExists: jest.fn(() => false), + }; let timeSrv = { time: { from: 'now-1h', to: 'now' }, @@ -68,8 +73,8 @@ describe('CloudWatchDatasource', function() { }, }; - beforeEach(async () => { - ctx.backendSrv.datasourceRequest = await jest.fn(params => { + beforeEach(() => { + ctx.backendSrv.datasourceRequest = jest.fn(params => { requestParams = params.data; return Promise.resolve({ data: response }); }); @@ -123,103 +128,6 @@ describe('CloudWatchDatasource', function() { done(); }); }); - - it('should generate the correct targets by expanding template variables', function() { - var templateSrv = { - variables: [ - { - name: 'instance_id', - options: [ - { text: 'i-23456789', value: 'i-23456789', selected: false }, - { text: 'i-34567890', value: 'i-34567890', selected: true }, - ], - current: { - text: 'i-34567890', - value: 'i-34567890', - }, - }, - ], - replace: function(target, scopedVars) { - if (target === '$instance_id' && scopedVars['instance_id']['text'] === 'i-34567890') { - return 'i-34567890'; - } else { - return ''; - } - }, - getVariableName: function(e) { - return 'instance_id'; - }, - variableExists: function(e) { - return true; - }, - containsVariable: function(str, variableName) { - return str.indexOf('$' + variableName) !== -1; - }, - }; - - var targets = [ - { - region: 'us-east-1', - namespace: 'AWS/EC2', - metricName: 'CPUUtilization', - dimensions: { - InstanceId: '$instance_id', - }, - statistics: ['Average'], - period: 300, - }, - ]; - - var result = ctx.ds.expandTemplateVariable(targets, {}, templateSrv); - expect(result[0].dimensions.InstanceId).toBe('i-34567890'); - }); - - it('should generate the correct targets by expanding template variables from url', function() { - var templateSrv = { - variables: [ - { - name: 'instance_id', - options: [ - { text: 'i-23456789', value: 'i-23456789', selected: false }, - { text: 'i-34567890', value: 'i-34567890', selected: false }, - ], - current: 'i-45678901', - }, - ], - replace: function(target, scopedVars) { - if (target === '$instance_id') { - return 'i-45678901'; - } else { - return ''; - } - }, - getVariableName: function(e) { - return 'instance_id'; - }, - variableExists: function(e) { - return true; - }, - containsVariable: function(str, variableName) { - return str.indexOf('$' + variableName) !== -1; - }, - }; - - var targets = [ - { - region: 'us-east-1', - namespace: 'AWS/EC2', - metricName: 'CPUUtilization', - dimensions: { - InstanceId: '$instance_id', - }, - statistics: ['Average'], - period: 300, - }, - ]; - - var result = ctx.ds.expandTemplateVariable(targets, {}, templateSrv); - expect(result[0].dimensions.InstanceId).toBe('i-45678901'); - }); }); describe('When query region is "default"', function() { From e51f208a49b4997de43495d9b3c16e7d6d790c37 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 9 Jul 2018 15:42:34 +0200 Subject: [PATCH 023/410] Cleanup and remove some jest.fn() --- .../datasource/cloudwatch/specs/datasource.jest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts index b06d00b3ee4..2dc6e57b1aa 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts @@ -1,6 +1,5 @@ import '../datasource'; import CloudWatchDatasource from '../datasource'; -import 'app/features/dashboard/time_srv'; import * as dateMath from 'app/core/utils/datemath'; import _ from 'lodash'; @@ -10,19 +9,20 @@ describe('CloudWatchDatasource', function() { }; let templateSrv = { + data: {}, templateSettings: { interpolate: /\[\[([\s\S]+?)\]\]/g }, - replace: jest.fn(text => _.template(text, templateSrv.templateSettings)(templateSrv.data)), - variableExists: jest.fn(() => false), + replace: text => _.template(text, templateSrv.templateSettings)(templateSrv.data), + variableExists: () => false, }; let timeSrv = { time: { from: 'now-1h', to: 'now' }, - timeRange: jest.fn(() => { + timeRange: () => { return { from: dateMath.parse(timeSrv.time.from, false), to: dateMath.parse(timeSrv.time.to, true), }; - }), + }, }; let backendSrv = {}; let ctx = { From 8b32dc584705c87a45326cd185b19810d5d120e0 Mon Sep 17 00:00:00 2001 From: yogy rahmawan Date: Tue, 10 Jul 2018 13:25:32 +0700 Subject: [PATCH 024/410] move go vet out of scripts and fixing warning (#12552) --- .circleci/config.yml | 3 +++ pkg/services/notifications/notifications.go | 2 -- pkg/services/rendering/phantomjs.go | 4 +++- pkg/tsdb/elasticsearch/client/client.go | 2 +- pkg/tsdb/elasticsearch/client/models.go | 2 +- scripts/circle-test-backend.sh | 3 --- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f351040fe2f..f1d161c3cac 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,6 +88,9 @@ jobs: - run: 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 + command: 'go vet ./pkg/...' test-frontend: docker: diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index fcefa91243d..769fdd06fd0 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -98,8 +98,6 @@ func (ns *NotificationService) Run(ctx context.Context) error { return ctx.Err() } } - - return nil } func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { diff --git a/pkg/services/rendering/phantomjs.go b/pkg/services/rendering/phantomjs.go index 8e06b5fed9d..87ccaf6b5d2 100644 --- a/pkg/services/rendering/phantomjs.go +++ b/pkg/services/rendering/phantomjs.go @@ -58,7 +58,9 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) ( cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", opts.Encoding)}, cmdArgs...) } - commandCtx, _ := context.WithTimeout(ctx, opts.Timeout+time.Second*2) + commandCtx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2) + defer cancel() + cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...) cmd.Stderr = cmd.Stdout diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index efc3ed1bda2..dff626a79eb 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -218,7 +218,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch elapsed := time.Now().Sub(start) clientLog.Debug("Decoded multisearch json response", "took", elapsed) - msr.status = res.StatusCode + msr.Status = res.StatusCode return &msr, nil } diff --git a/pkg/tsdb/elasticsearch/client/models.go b/pkg/tsdb/elasticsearch/client/models.go index a5810a9b109..a0d257d01a6 100644 --- a/pkg/tsdb/elasticsearch/client/models.go +++ b/pkg/tsdb/elasticsearch/client/models.go @@ -74,7 +74,7 @@ type MultiSearchRequest struct { // MultiSearchResponse represents a multi search response type MultiSearchResponse struct { - status int `json:"status,omitempty"` + Status int `json:"status,omitempty"` Responses []*SearchResponse `json:"responses"` } diff --git a/scripts/circle-test-backend.sh b/scripts/circle-test-backend.sh index a63d6354fa6..4740ef99f1a 100755 --- a/scripts/circle-test-backend.sh +++ b/scripts/circle-test-backend.sh @@ -13,9 +13,6 @@ function exit_if_fail { echo "running go fmt" exit_if_fail test -z "$(gofmt -s -l ./pkg | tee /dev/stderr)" -echo "running go vet" -exit_if_fail test -z "$(go vet ./pkg/... | tee /dev/stderr)" - echo "building backend with install to cache pkgs" exit_if_fail time go install ./pkg/cmd/grafana-server From 55780972109190b57b3b1fd48a899ec35466e92e Mon Sep 17 00:00:00 2001 From: Dominic Smith Date: Tue, 10 Jul 2018 19:17:55 +1000 Subject: [PATCH 025/410] Add new sequential color scales * Add new sequential color scales * Simplify inversion logic --- package.json | 2 +- public/app/plugins/panel/heatmap/color_scale.ts | 2 +- public/app/plugins/panel/heatmap/heatmap_ctrl.ts | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a43b2adc5be..c26438230cc 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "classnames": "^2.2.5", "clipboard": "^1.7.1", "d3": "^4.11.0", - "d3-scale-chromatic": "^1.1.1", + "d3-scale-chromatic": "^1.3.0", "eventemitter3": "^2.0.3", "file-saver": "^1.3.3", "immutable": "^3.8.2", diff --git a/public/app/plugins/panel/heatmap/color_scale.ts b/public/app/plugins/panel/heatmap/color_scale.ts index 40828639547..3550c981db2 100644 --- a/public/app/plugins/panel/heatmap/color_scale.ts +++ b/public/app/plugins/panel/heatmap/color_scale.ts @@ -3,7 +3,7 @@ import * as d3ScaleChromatic from 'd3-scale-chromatic'; export function getColorScale(colorScheme: any, lightTheme: boolean, maxValue: number, minValue = 0): (d: any) => any { let colorInterpolator = d3ScaleChromatic[colorScheme.value]; - let colorScaleInverted = colorScheme.invert === 'always' || (colorScheme.invert === 'dark' && !lightTheme); + let colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark'); let start = colorScaleInverted ? maxValue : minValue; let end = colorScaleInverted ? minValue : maxValue; diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 31a5afa630e..1d35ff2ea84 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -76,6 +76,13 @@ let colorSchemes = [ { name: 'Reds', value: 'interpolateReds', invert: 'dark' }, // Sequential (Multi-Hue) + { name: 'Viridis', value: 'interpolateViridis', invert: 'light' }, + { name: 'Magma', value: 'interpolateMagma', invert: 'light' }, + { name: 'Inferno', value: 'interpolateInferno', invert: 'light' }, + { name: 'Plasma', value: 'interpolatePlasma', invert: 'light' }, + { name: 'Warm', value: 'interpolateWarm', invert: 'light' }, + { name: 'Cool', value: 'interpolateCool', invert: 'light' }, + { name: 'Cubehelix', value: 'interpolateCubehelixDefault', invert: 'light' }, { name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' }, { name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' }, { name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' }, @@ -87,7 +94,7 @@ let colorSchemes = [ { name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' }, { name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' }, { name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' }, - { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }, + { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' }, ]; const ds_support_histogram_sort = ['prometheus', 'elasticsearch']; From 4ee4ca99be159862a8990034e0087417174dfd09 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Tue, 10 Jul 2018 12:54:45 +0200 Subject: [PATCH 026/410] 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" Date: Tue, 10 Jul 2018 10:11:39 -0600 Subject: [PATCH 027/410] 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 4d2dd22095ba4f0f96a2d524cb43a7d71e771e5f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 11 Jul 2018 12:29:19 +0200 Subject: [PATCH 028/410] Fix css loading in plugins (#12573) - allow css loader to be imported again (wasnt prefixed by plugin) --- 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 f999ee7e2ff..20023e27b5c 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: { - 'plugin*': { + '*': { esModule: true, authorization: true, loader: 'plugin-loader', From cfa555b5695d76bf7e4033190f216885a38a38ad Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 11 Jul 2018 12:31:07 +0200 Subject: [PATCH 029/410] Add folder name to dashboard title (#12545) * Add folder name to dashboard title. No display on medium displays and below * Compare folderId instead * folderId bigger than 0 --- public/app/features/dashboard/dashnav/dashnav.html | 2 +- public/sass/components/_navbar.scss | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 269d4b0bada..6ec272b5ca4 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -3,7 +3,7 @@ diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 1a453b15069..0b7e3a79dcd 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -85,6 +85,14 @@ // icon hidden on smaller screens display: none; } + + &--folder { + color: $text-color-weak; + + @include media-breakpoint-down(md) { + display: none; + } + } } .navbar-buttons { From 2fbf2bba4e61af9ad7ea0c870364138387bd4400 Mon Sep 17 00:00:00 2001 From: Rollulus Rouloul Date: Wed, 11 Jul 2018 12:33:19 +0200 Subject: [PATCH 030/410] fix: requests/sec instead of requets (#12557) --- 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 4302e62e3e0..463025567cd 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -957,7 +957,7 @@ kbn.getUnitFormats = function() { text: 'throughput', submenu: [ { text: 'ops/sec (ops)', value: 'ops' }, - { text: 'requets/sec (rps)', value: 'reqps' }, + { text: 'requests/sec (rps)', value: 'reqps' }, { text: 'reads/sec (rps)', value: 'rps' }, { text: 'writes/sec (wps)', value: 'wps' }, { text: 'I/O ops/sec (iops)', value: 'iops' }, From 2c22a7b4ba12966cac8e092167cb45c5fdadfeb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 Jul 2018 13:31:34 +0200 Subject: [PATCH 031/410] Don't build-all for PRs --- .circleci/config.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f1d161c3cac..44f34d42926 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -246,7 +246,7 @@ workflows: test-and-build: jobs: - build-all: - filters: *filter-not-release + filters: *filter-only-master - build-enterprise: filters: *filter-only-master - codespell: @@ -270,9 +270,7 @@ workflows: - gometalinter - mysql-integration-test - postgres-integration-test - filters: - branches: - only: master + filters: *filter-only-master - deploy-enterprise-master: requires: - build-all From 24f395f986d413606675303988c2eb4d93694853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Gryglicki?= Date: Wed, 11 Jul 2018 19:06:36 +0200 Subject: [PATCH 032/410] Add support for skipping variable value in URL, fixes #12174 (#12541) * New rebase Signed-off-by: Lukasz Gryglicki * Lint Signed-off-by: Lukasz Gryglicki --- .../app/features/templating/adhoc_variable.ts | 2 + .../features/templating/constant_variable.ts | 2 + .../features/templating/custom_variable.ts | 2 + .../templating/datasource_variable.ts | 2 + .../features/templating/interval_variable.ts | 2 + .../app/features/templating/query_variable.ts | 2 + .../templating/specs/template_srv.jest.ts | 57 +++++++++++++++++++ .../app/features/templating/template_srv.ts | 6 ++ 8 files changed, 75 insertions(+) diff --git a/public/app/features/templating/adhoc_variable.ts b/public/app/features/templating/adhoc_variable.ts index babeaf1f34e..9f8bd4c39a7 100644 --- a/public/app/features/templating/adhoc_variable.ts +++ b/public/app/features/templating/adhoc_variable.ts @@ -3,6 +3,7 @@ import { Variable, assignModelProperties, variableTypes } from './variable'; export class AdhocVariable implements Variable { filters: any[]; + skipUrlSync: boolean; defaults = { type: 'adhoc', @@ -11,6 +12,7 @@ export class AdhocVariable implements Variable { hide: 0, datasource: null, filters: [], + skipUrlSync: false, }; /** @ngInject **/ diff --git a/public/app/features/templating/constant_variable.ts b/public/app/features/templating/constant_variable.ts index f2fb4294537..e727c6e98af 100644 --- a/public/app/features/templating/constant_variable.ts +++ b/public/app/features/templating/constant_variable.ts @@ -4,6 +4,7 @@ export class ConstantVariable implements Variable { query: string; options: any[]; current: any; + skipUrlSync: boolean; defaults = { type: 'constant', @@ -13,6 +14,7 @@ export class ConstantVariable implements Variable { query: '', current: {}, options: [], + skipUrlSync: false, }; /** @ngInject **/ diff --git a/public/app/features/templating/custom_variable.ts b/public/app/features/templating/custom_variable.ts index c15178f6644..4490a41a38f 100644 --- a/public/app/features/templating/custom_variable.ts +++ b/public/app/features/templating/custom_variable.ts @@ -7,6 +7,7 @@ export class CustomVariable implements Variable { includeAll: boolean; multi: boolean; current: any; + skipUrlSync: boolean; defaults = { type: 'custom', @@ -19,6 +20,7 @@ export class CustomVariable implements Variable { includeAll: false, multi: false, allValue: null, + skipUrlSync: false, }; /** @ngInject **/ diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts index 4c326a94e3b..519ce21e4d4 100644 --- a/public/app/features/templating/datasource_variable.ts +++ b/public/app/features/templating/datasource_variable.ts @@ -7,6 +7,7 @@ export class DatasourceVariable implements Variable { options: any; current: any; refresh: any; + skipUrlSync: boolean; defaults = { type: 'datasource', @@ -18,6 +19,7 @@ export class DatasourceVariable implements Variable { options: [], query: '', refresh: 1, + skipUrlSync: false, }; /** @ngInject **/ diff --git a/public/app/features/templating/interval_variable.ts b/public/app/features/templating/interval_variable.ts index 3faac316f98..b932819a7b7 100644 --- a/public/app/features/templating/interval_variable.ts +++ b/public/app/features/templating/interval_variable.ts @@ -11,6 +11,7 @@ export class IntervalVariable implements Variable { query: string; refresh: number; current: any; + skipUrlSync: boolean; defaults = { type: 'interval', @@ -24,6 +25,7 @@ export class IntervalVariable implements Variable { auto: false, auto_min: '10s', auto_count: 30, + skipUrlSync: false, }; /** @ngInject **/ diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 54bd7bb660c..5ddd6d32864 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -22,6 +22,7 @@ export class QueryVariable implements Variable { tagsQuery: string; tagValuesQuery: string; tags: any[]; + skipUrlSync: boolean; defaults = { type: 'query', @@ -42,6 +43,7 @@ export class QueryVariable implements Variable { useTags: false, tagsQuery: '', tagValuesQuery: '', + skipUrlSync: false, }; /** @ngInject **/ diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 59915776b4f..86b6aa7ec99 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -345,6 +345,49 @@ describe('templateSrv', function() { }); }); + describe('fillVariableValuesForUrl skip url sync', function() { + beforeEach(function() { + initTemplateSrv([ + { + name: 'test', + skipUrlSync: true, + current: { value: 'value' }, + getValueForUrl: function() { + return this.current.value; + }, + }, + ]); + }); + + it('should not include template variable value in url', function() { + var params = {}; + _templateSrv.fillVariableValuesForUrl(params); + expect(params['var-test']).toBe(undefined); + }); + }); + + describe('fillVariableValuesForUrl with multi value with skip url sync', function() { + beforeEach(function() { + initTemplateSrv([ + { + type: 'query', + name: 'test', + skipUrlSync: true, + current: { value: ['val1', 'val2'] }, + getValueForUrl: function() { + return this.current.value; + }, + }, + ]); + }); + + it('should not include template variable value in url', function() { + var params = {}; + _templateSrv.fillVariableValuesForUrl(params); + expect(params['var-test']).toBe(undefined); + }); + }); + describe('fillVariableValuesForUrl with multi value and scopedVars', function() { beforeEach(function() { initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]); @@ -359,6 +402,20 @@ describe('templateSrv', function() { }); }); + describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', function() { + beforeEach(function() { + initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]); + }); + + it('should not set scoped value as url params', function() { + var params = {}; + _templateSrv.fillVariableValuesForUrl(params, { + test: { name: 'test', value: 'val1', skipUrlSync: true }, + }); + expect(params['var-test']).toBe(undefined); + }); + }); + describe('replaceWithText', function() { beforeEach(function() { initTemplateSrv([ diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index cdabe577f96..fc79d12ff9e 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -250,8 +250,14 @@ export class TemplateSrv { fillVariableValuesForUrl(params, scopedVars) { _.each(this.variables, function(variable) { if (scopedVars && scopedVars[variable.name] !== void 0) { + if (scopedVars[variable.name].skipUrlSync) { + return; + } params['var-' + variable.name] = scopedVars[variable.name].value; } else { + if (variable.skipUrlSync) { + return; + } params['var-' + variable.name] = variable.getValueForUrl(); } }); From b4e0ace7a2f5d50d84745726349bd4a475aa7776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 Jul 2018 10:58:06 -0700 Subject: [PATCH 033/410] fix: folder picker did not notify parent that the initial folder had been changed, fixes #12543 (#12554) --- .../dashboard/folder_picker/folder_picker.ts | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts index 28338c29d33..352b29d27a0 100644 --- a/public/app/features/dashboard/folder_picker/folder_picker.ts +++ b/public/app/features/dashboard/folder_picker/folder_picker.ts @@ -104,10 +104,7 @@ export class FolderPickerCtrl { appEvents.emit('alert-success', ['Folder Created', 'OK']); this.closeCreateFolder(); - this.folder = { - text: result.title, - value: result.id, - }; + this.folder = { text: result.title, value: result.id }; this.onFolderChange(this.folder); }); } @@ -149,17 +146,14 @@ export class FolderPickerCtrl { folder = result.length > 0 ? result[0] : resetFolder; } } - this.folder = folder; - this.onFolderLoad(); - }); - } - private onFolderLoad() { - if (this.onLoad) { - this.onLoad({ - $folder: { id: this.folder.value, title: this.folder.text }, - }); - } + this.folder = folder; + + // if this is not the same as our initial value notify parent + if (this.folder.id !== this.initialFolderId) { + this.onChange({ $folder: { id: this.folder.value, title: this.folder.text } }); + } + }); } } @@ -176,7 +170,6 @@ export function folderPicker() { labelClass: '@', rootName: '@', onChange: '&', - onLoad: '&', onCreateFolder: '&', enterFolderCreation: '&', exitFolderCreation: '&', From 18a8290c65007bf86c074dc5f3f2b1bdb7c6c3d4 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 12 Jul 2018 03:13:47 +0900 Subject: [PATCH 034/410] (prometheus) prevent error to use $__interval_ms in query (#12533) * prevent error to use $__interval_ms in query * add test * prevent error to use $__interval_ms in query --- .../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 75c0de3bc6e..6eb6d3b3b00 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: this.intervalMs, value: this.intervalMs }, + __interval_ms: { text: String(this.intervalMs), value: String(this.intervalMs) }, }); var metricsQuery = { diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index d7d33264c99..88d6141696d 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -196,7 +196,7 @@ export class PrometheusDatasource { interval = adjustedInterval; scopedVars = Object.assign({}, options.scopedVars, { __interval: { text: interval + 's', value: interval + 's' }, - __interval_ms: { text: interval * 1000, value: interval * 1000 }, + __interval_ms: { text: String(interval * 1000), value: String(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 c5da671b757..09aa934dd63 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: 10 * 1000, value: 10 * 1000 }, + __interval_ms: { text: String(10 * 1000), value: String(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(10 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000); + expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000)); + expect(query.scopedVars.__interval_ms.value).to.be(String(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: 5 * 1000, value: 5 * 1000 }, + __interval_ms: { text: String(5 * 1000), value: String(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(5 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); + expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000)); + expect(query.scopedVars.__interval_ms.value).to.be(String(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: 10 * 1000, value: 10 * 1000 }, + __interval_ms: { text: String(10 * 1000), value: String(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(10 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000); + expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000)); + expect(query.scopedVars.__interval_ms.value).to.be(String(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: 5 * 1000, value: 5 * 1000 }, + __interval_ms: { text: String(5 * 1000), value: String(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(5 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); + expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000)); + expect(query.scopedVars.__interval_ms.value).to.be(String(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: 5 * 1000, value: 5 * 1000 }, + __interval_ms: { text: String(5 * 1000), value: String(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(5 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); + expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000)); + expect(query.scopedVars.__interval_ms.value).to.be(String(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: 5 * 1000, value: 5 * 1000 }, + __interval_ms: { text: String(5 * 1000), value: String(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(5 * 1000); - expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); + expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000)); + expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000)); }); }); }); From c03764ff8a47bcc9bcd007de2345baa53d80294e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 Jul 2018 11:23:07 -0700 Subject: [PATCH 035/410] Refactor team pages to react & design change (#12574) * Rewriting team pages in react * teams to react progress * teams: getting team by id returns same DTO as search, needed for AvatarUrl * teams: progress on new team pages * fix: team test * listing team members and removing team members now works * teams: team member page now works * ux: fixed adding team member issue * refactoring TeamPicker to conform to react coding styles better * teams: very close to being done with team page rewrite * minor style tweak * ux: polish to team pages * feature: team pages in react & everything working * fix: removed flickering when changing tabs by always rendering PageHeader --- pkg/api/alerting_test.go | 2 +- pkg/api/annotations_test.go | 2 +- pkg/api/dashboard_snapshot_test.go | 2 +- pkg/api/dashboard_test.go | 4 +- pkg/api/team.go | 1 + pkg/api/team_test.go | 2 +- pkg/models/team.go | 16 +- pkg/services/guardian/guardian.go | 10 +- pkg/services/guardian/guardian_util_test.go | 6 +- pkg/services/sqlstore/team.go | 44 +++-- .../ManageDashboards/FolderPermissions.tsx | 4 +- public/app/containers/Teams/TeamGroupSync.tsx | 149 +++++++++++++++++ public/app/containers/Teams/TeamList.tsx | 125 ++++++++++++++ public/app/containers/Teams/TeamMembers.tsx | 144 ++++++++++++++++ public/app/containers/Teams/TeamPages.tsx | 77 +++++++++ public/app/containers/Teams/TeamSettings.tsx | 69 ++++++++ public/app/core/angular_wrappers.ts | 2 - public/app/core/components/Forms/Forms.tsx | 21 +++ .../Permissions/AddPermissions.jest.tsx | 48 +++--- .../components/Permissions/AddPermissions.tsx | 61 +++---- .../Permissions/DashboardPermissions.tsx | 7 +- .../DisabledPermissionsListItem.tsx | 2 +- .../Permissions/PermissionsListItem.tsx | 2 +- .../components/Picker/DescriptionPicker.tsx | 10 +- .../components/Picker/TeamPicker.jest.tsx | 24 +-- .../app/core/components/Picker/TeamPicker.tsx | 38 ++--- .../components/Picker/UserPicker.jest.tsx | 21 +-- .../app/core/components/Picker/UserPicker.tsx | 57 ++++--- .../app/core/components/Picker/withPicker.tsx | 34 ---- public/app/core/components/grafana_app.ts | 4 +- public/app/core/components/team_picker.ts | 64 ------- public/app/core/components/user_picker.ts | 71 -------- public/app/core/core.ts | 4 - public/app/core/services/backend_srv.ts | 14 ++ public/app/features/org/all.ts | 2 - .../features/org/partials/team_details.html | 105 ------------ public/app/features/org/partials/teams.html | 68 -------- .../org/specs/team_details_ctrl.jest.ts | 42 ----- public/app/features/org/team_details_ctrl.ts | 108 ------------ public/app/features/org/teams_ctrl.ts | 66 -------- public/app/routes/routes.ts | 20 ++- public/app/stores/NavStore/NavItem.ts | 3 +- public/app/stores/NavStore/NavStore.ts | 40 +++++ public/app/stores/RootStore/RootStore.ts | 4 + public/app/stores/TeamsStore/TeamsStore.ts | 156 ++++++++++++++++++ public/sass/components/_gf-form.scss | 4 +- public/test/jest-shim.ts | 13 +- 47 files changed, 1015 insertions(+), 757 deletions(-) create mode 100644 public/app/containers/Teams/TeamGroupSync.tsx create mode 100644 public/app/containers/Teams/TeamList.tsx create mode 100644 public/app/containers/Teams/TeamMembers.tsx create mode 100644 public/app/containers/Teams/TeamPages.tsx create mode 100644 public/app/containers/Teams/TeamSettings.tsx create mode 100644 public/app/core/components/Forms/Forms.tsx delete mode 100644 public/app/core/components/Picker/withPicker.tsx delete mode 100644 public/app/core/components/team_picker.ts delete mode 100644 public/app/core/components/user_picker.ts delete mode 100644 public/app/features/org/partials/team_details.html delete mode 100755 public/app/features/org/partials/teams.html delete mode 100644 public/app/features/org/specs/team_details_ctrl.jest.ts delete mode 100644 public/app/features/org/team_details_ctrl.ts delete mode 100644 public/app/features/org/teams_ctrl.ts create mode 100644 public/app/stores/TeamsStore/TeamsStore.ts diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 9eba0e0d5b6..331beeef5e4 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -31,7 +31,7 @@ func TestAlertingApiEndpoint(t *testing.T) { }) bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = []*m.Team{} + query.Result = []*m.TeamDTO{} return nil }) diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 6590eb19ff2..08f3018c694 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -119,7 +119,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) { }) bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = []*m.Team{} + query.Result = []*m.TeamDTO{} return nil }) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index 5e7637a24e1..e58f2c4712d 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -39,7 +39,7 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { return nil }) - teamResp := []*m.Team{} + teamResp := []*m.TeamDTO{} bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { query.Result = teamResp return nil diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 50a2e314f5c..283a9b5f12c 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -61,7 +61,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = []*m.Team{} + query.Result = []*m.TeamDTO{} return nil }) @@ -230,7 +230,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = []*m.Team{} + query.Result = []*m.TeamDTO{} return nil }) diff --git a/pkg/api/team.go b/pkg/api/team.go index 9919305881b..ebb426c4c82 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -93,5 +93,6 @@ func GetTeamByID(c *m.ReqContext) Response { return Error(500, "Failed to get Team", err) } + query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name) return JSON(200, &query.Result) } diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index 0bf06d723c8..a1984288870 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -13,7 +13,7 @@ import ( func TestTeamApiEndpoint(t *testing.T) { Convey("Given two teams", t, func() { mockResult := models.SearchTeamQueryResult{ - Teams: []*models.SearchTeamDto{ + Teams: []*models.TeamDTO{ {Name: "team1"}, {Name: "team2"}, }, diff --git a/pkg/models/team.go b/pkg/models/team.go index 9c679a13394..61285db3a5f 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -49,13 +49,13 @@ type DeleteTeamCommand struct { type GetTeamByIdQuery struct { OrgId int64 Id int64 - Result *Team + Result *TeamDTO } type GetTeamsByUserQuery struct { OrgId int64 - UserId int64 `json:"userId"` - Result []*Team `json:"teams"` + UserId int64 `json:"userId"` + Result []*TeamDTO `json:"teams"` } type SearchTeamsQuery struct { @@ -68,7 +68,7 @@ type SearchTeamsQuery struct { Result SearchTeamQueryResult } -type SearchTeamDto struct { +type TeamDTO struct { Id int64 `json:"id"` OrgId int64 `json:"orgId"` Name string `json:"name"` @@ -78,8 +78,8 @@ type SearchTeamDto struct { } type SearchTeamQueryResult struct { - TotalCount int64 `json:"totalCount"` - Teams []*SearchTeamDto `json:"teams"` - Page int `json:"page"` - PerPage int `json:"perPage"` + TotalCount int64 `json:"totalCount"` + Teams []*TeamDTO `json:"teams"` + Page int `json:"page"` + PerPage int `json:"perPage"` } diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index cfd8f5c3a6e..7506338c5f0 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -30,7 +30,7 @@ type dashboardGuardianImpl struct { dashId int64 orgId int64 acl []*m.DashboardAclInfoDTO - groups []*m.Team + teams []*m.TeamDTO log log.Logger } @@ -186,15 +186,15 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) { return g.acl, nil } -func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) { - if g.groups != nil { - return g.groups, nil +func (g *dashboardGuardianImpl) getTeams() ([]*m.TeamDTO, error) { + if g.teams != nil { + return g.teams, nil } query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId} err := bus.Dispatch(&query) - g.groups = query.Result + g.teams = query.Result return query.Result, err } diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go index 3d839e71b74..d85548ecb8c 100644 --- a/pkg/services/guardian/guardian_util_test.go +++ b/pkg/services/guardian/guardian_util_test.go @@ -19,7 +19,7 @@ type scenarioContext struct { givenUser *m.SignedInUser givenDashboardID int64 givenPermissions []*m.DashboardAclInfoDTO - givenTeams []*m.Team + givenTeams []*m.TeamDTO updatePermissions []*m.DashboardAcl expectedFlags permissionFlags callerFile string @@ -84,11 +84,11 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, per return nil }) - teams := []*m.Team{} + teams := []*m.TeamDTO{} for _, p := range permissions { if p.TeamId > 0 { - teams = append(teams, &m.Team{Id: p.TeamId}) + teams = append(teams, &m.TeamDTO{Id: p.TeamId}) } } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 9378ca37f60..72955df9a6a 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -22,6 +22,16 @@ func init() { bus.AddHandler("sql", GetTeamMembers) } +func getTeamSelectSqlBase() string { + return `SELECT + team.id as id, + team.org_id, + team.name as name, + team.email as email, + (SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count + FROM team as team ` +} + func CreateTeam(cmd *m.CreateTeamCommand) error { return inTransaction(func(sess *DBSession) error { @@ -130,21 +140,15 @@ func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession func SearchTeams(query *m.SearchTeamsQuery) error { query.Result = m.SearchTeamQueryResult{ - Teams: make([]*m.SearchTeamDto, 0), + Teams: make([]*m.TeamDTO, 0), } queryWithWildcards := "%" + query.Query + "%" var sql bytes.Buffer params := make([]interface{}, 0) - sql.WriteString(`select - team.id as id, - team.org_id, - team.name as name, - team.email as email, - (select count(*) from team_member where team_member.team_id = team.id) as member_count - from team as team - where team.org_id = ?`) + sql.WriteString(getTeamSelectSqlBase()) + sql.WriteString(` WHERE team.org_id = ?`) params = append(params, query.OrgId) @@ -186,8 +190,14 @@ func SearchTeams(query *m.SearchTeamsQuery) error { } func GetTeamById(query *m.GetTeamByIdQuery) error { - var team m.Team - exists, err := x.Where("org_id=? and id=?", query.OrgId, query.Id).Get(&team) + var sql bytes.Buffer + + sql.WriteString(getTeamSelectSqlBase()) + sql.WriteString(` WHERE team.org_id = ? and team.id = ?`) + + var team m.TeamDTO + exists, err := x.Sql(sql.String(), query.OrgId, query.Id).Get(&team) + if err != nil { return err } @@ -202,13 +212,15 @@ func GetTeamById(query *m.GetTeamByIdQuery) error { // GetTeamsByUser is used by the Guardian when checking a users' permissions func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { - query.Result = make([]*m.Team, 0) + query.Result = make([]*m.TeamDTO, 0) - sess := x.Table("team") - sess.Join("INNER", "team_member", "team.id=team_member.team_id") - sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId) + var sql bytes.Buffer - err := sess.Find(&query.Result) + sql.WriteString(getTeamSelectSqlBase()) + sql.WriteString(` INNER JOIN team_member on team.id = team_member.team_id`) + sql.WriteString(` WHERE team.org_id = ? and team_member.user_id = ?`) + + err := x.Sql(sql.String(), query.OrgId, query.UserId).Find(&query.Result) return err } diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index abbde63a179..aac5d32750a 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -54,7 +54,7 @@ export class FolderPermissions extends Component {
    -

    Folder Permissions

    +

    Folder Permissions

    @@ -68,7 +68,7 @@ export class FolderPermissions extends Component {
    - +
    diff --git a/public/app/containers/Teams/TeamGroupSync.tsx b/public/app/containers/Teams/TeamGroupSync.tsx new file mode 100644 index 00000000000..323dceae0d8 --- /dev/null +++ b/public/app/containers/Teams/TeamGroupSync.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { observer } from 'mobx-react'; +import { ITeam, ITeamGroup } 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; +} + +interface State { + isAdding: boolean; + newGroupId?: string; +} + +const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; + +@observer +export class TeamGroupSync extends React.Component { + constructor(props) { + super(props); + this.state = { isAdding: false, newGroupId: '' }; + } + + componentDidMount() { + this.props.team.loadGroups(); + } + + renderGroup(group: ITeamGroup) { + return ( + + {group.groupId} + + this.onRemoveGroup(group)}> + + + + + ); + } + + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onNewGroupIdChanged = evt => { + this.setState({ newGroupId: evt.target.value }); + }; + + onAddGroup = () => { + this.props.team.addGroup(this.state.newGroupId); + this.setState({ isAdding: false, newGroupId: '' }); + }; + + onRemoveGroup = (group: ITeamGroup) => { + this.props.team.removeGroup(group.groupId); + }; + + isNewGroupValid() { + return this.state.newGroupId.length > 1; + } + + render() { + const { isAdding, newGroupId } = this.state; + const groups = this.props.team.groups.values(); + + return ( +
    +
    +

    External group sync

    + + + +
    + {groups.length > 0 && ( + + )} +
    + + +
    + +
    Add External Group
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + + {groups.length === 0 && + !isAdding && ( +
    +
    There are no external groups to sync with
    + +
    + {headerTooltip} + + Learn more + +
    +
    + )} + + {groups.length > 0 && ( +
    + + + + + + + {groups.map(group => this.renderGroup(group))} +
    External Group ID +
    +
    + )} +
    + ); + } +} + +export default hot(module)(TeamGroupSync); diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx new file mode 100644 index 00000000000..4429764b1cc --- /dev/null +++ b/public/app/containers/Teams/TeamList.tsx @@ -0,0 +1,125 @@ +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 { 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'; + +interface Props { + nav: typeof NavStore.Type; + teams: typeof TeamsStore.Type; + backendSrv: BackendSrv; +} + +@inject('nav', 'teams') +@observer +export class TeamList extends React.Component { + constructor(props) { + super(props); + + this.props.nav.load('cfg', 'teams'); + this.fetchTeams(); + } + + fetchTeams() { + this.props.teams.loadTeams(); + } + + 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)); + } + + onSearchQueryChange = evt => { + this.props.teams.setSearchQuery(evt.target.value); + }; + + renderTeamMember(team: ITeam): JSX.Element { + let teamUrl = `org/teams/edit/${team.id}`; + + return ( + + + + + + + + {team.name} + + + {team.email} + + + {team.memberCount} + + + this.deleteTeam(team)} className="btn btn-danger btn-small"> + + + + + ); + } + + render() { + const { nav, teams } = this.props; + return ( +
    + +
    +
    +
    + +
    + + + +
    + + + + + + + + + {teams.filteredTeams.map(team => this.renderTeamMember(team))} +
    + NameEmailMembers +
    +
    +
    +
    + ); + } +} + +export default hot(module)(TeamList); diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/containers/Teams/TeamMembers.tsx new file mode 100644 index 00000000000..0d0762469a0 --- /dev/null +++ b/public/app/containers/Teams/TeamMembers.tsx @@ -0,0 +1,144 @@ +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'; + +interface Props { + team: ITeam; +} + +interface State { + isAdding: boolean; + newTeamMember?: User; +} + +@observer +export class TeamMembers extends React.Component { + constructor(props) { + super(props); + this.state = { isAdding: false, newTeamMember: null }; + } + + componentDidMount() { + this.props.team.loadMembers(); + } + + onSearchQueryChange = evt => { + this.props.team.setSearchQuery(evt.target.value); + }; + + 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); + }, + }); + } + + removeMemberConfirmed(member: ITeamMember) { + this.props.team.removeMember(member); + } + + renderMember(member: ITeamMember) { + return ( + + + + + {member.login} + {member.email} + + this.removeMember(member)} className="btn btn-danger btn-mini"> + + + + + ); + } + + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onUserSelected = (user: User) => { + this.setState({ newTeamMember: user }); + }; + + onAddUserToTeam = async () => { + await this.props.team.addMember(this.state.newTeamMember.id); + await this.props.team.loadMembers(); + this.setState({ newTeamMember: null }); + }; + + render() { + const { newTeamMember, isAdding } = this.state; + const members = this.props.team.members.values(); + const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); + + return ( +
    +
    +
    + +
    + +
    + + +
    + + +
    + +
    Add Team Member
    +
    + + + {this.state.newTeamMember && ( + + )} +
    +
    +
    + +
    + + + + + + + + {members.map(member => this.renderMember(member))} +
    + NameEmail +
    +
    +
    + ); + } +} + +export default hot(module)(TeamMembers); diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/containers/Teams/TeamPages.tsx new file mode 100644 index 00000000000..500a7cbe5e8 --- /dev/null +++ b/public/app/containers/Teams/TeamPages.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import _ from 'lodash'; +import { hot } from 'react-hot-loader'; +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 { ViewStore } from 'app/stores/ViewStore/ViewStore'; +import TeamMembers from './TeamMembers'; +import TeamSettings from './TeamSettings'; +import TeamGroupSync from './TeamGroupSync'; + +interface Props { + nav: typeof NavStore.Type; + teams: typeof TeamsStore.Type; + view: typeof ViewStore.Type; +} + +@inject('nav', 'teams', 'view') +@observer +export class TeamPages extends React.Component { + isSyncEnabled: boolean; + currentPage: string; + + constructor(props) { + super(props); + + this.isSyncEnabled = config.buildInfo.isEnterprise; + this.currentPage = this.getCurrentPage(); + + this.loadTeam(); + } + + async loadTeam() { + const { teams, nav, view } = this.props; + + await teams.loadById(view.routeParams.get('id')); + + nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled); + } + + getCurrentTeam(): ITeam { + const { teams, view } = this.props; + return teams.map.get(view.routeParams.get('id')); + } + + getCurrentPage() { + const pages = ['members', 'settings', 'groupsync']; + const currentPage = this.props.view.routeParams.get('page'); + return _.includes(pages, currentPage) ? currentPage : pages[0]; + } + + render() { + const { nav } = this.props; + const currentTeam = this.getCurrentTeam(); + + if (!nav.main) { + return null; + } + + return ( +
    + + {currentTeam && ( +
    + {this.currentPage === 'members' && } + {this.currentPage === 'settings' && } + {this.currentPage === 'groupsync' && this.isSyncEnabled && } +
    + )} +
    + ); + } +} + +export default hot(module)(TeamPages); diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/containers/Teams/TeamSettings.tsx new file mode 100644 index 00000000000..142088a5d1e --- /dev/null +++ b/public/app/containers/Teams/TeamSettings.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { observer } from 'mobx-react'; +import { ITeam } from 'app/stores/TeamsStore/TeamsStore'; +import { Label } from 'app/core/components/Forms/Forms'; + +interface Props { + team: ITeam; +} + +@observer +export class TeamSettings extends React.Component { + constructor(props) { + super(props); + } + + onChangeName = evt => { + this.props.team.setName(evt.target.value); + }; + + onChangeEmail = evt => { + this.props.team.setEmail(evt.target.value); + }; + + onUpdate = evt => { + evt.preventDefault(); + this.props.team.update(); + }; + + render() { + return ( +
    +

    Team Settings

    +
    +
    + + +
    +
    + + +
    + +
    + +
    +
    +
    + ); + } +} + +export default hot(module)(TeamSettings); diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index ace0eb00b07..a4439509f8e 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import LoginBackground from './components/Login/LoginBackground'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; -import UserPicker from './components/Picker/UserPicker'; import DashboardPermissions from './components/Permissions/DashboardPermissions'; export function registerAngularDirectives() { @@ -19,6 +18,5 @@ export function registerAngularDirectives() { ['onSelect', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); - react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']); react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); } diff --git a/public/app/core/components/Forms/Forms.tsx b/public/app/core/components/Forms/Forms.tsx new file mode 100644 index 00000000000..4b74d48ba08 --- /dev/null +++ b/public/app/core/components/Forms/Forms.tsx @@ -0,0 +1,21 @@ +import React, { SFC, ReactNode } from 'react'; +import Tooltip from '../Tooltip/Tooltip'; + +interface Props { + tooltip?: string; + for?: string; + children: ReactNode; +} + +export const Label: SFC = props => { + return ( + + {props.children} + {props.tooltip && ( + + + + )} + + ); +}; diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.jest.tsx index fe97c4c7e62..513a22ddea4 100644 --- a/public/app/core/components/Permissions/AddPermissions.jest.tsx +++ b/public/app/core/components/Permissions/AddPermissions.jest.tsx @@ -1,32 +1,32 @@ -import React from 'react'; +import React from 'react'; +import { shallow } from 'enzyme'; import AddPermissions from './AddPermissions'; import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; -import { shallow } from 'enzyme'; +import { getBackendSrv } from 'app/core/services/backend_srv'; + +jest.mock('app/core/services/backend_srv', () => ({ + getBackendSrv: () => { + return { + get: () => { + return Promise.resolve([ + { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, + { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, + ]); + }, + post: jest.fn(() => Promise.resolve({})), + }; + }, +})); describe('AddPermissions', () => { let wrapper; let store; let instance; + let backendSrv: any = getBackendSrv(); beforeAll(() => { - backendSrv.get.mockReturnValue( - Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - ]) - ); - - backendSrv.post = jest.fn(() => Promise.resolve({})); - - store = RootStore.create( - {}, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); + store = RootStore.create({}, { backendSrv: backendSrv }); + wrapper = shallow(); instance = wrapper.instance(); return store.permissions.load(1, true, false); }); @@ -43,8 +43,8 @@ describe('AddPermissions', () => { login: 'user2', }; - instance.typeChanged(evt); - instance.userPicked(userItem); + instance.onTypeChanged(evt); + instance.onUserSelected(userItem); wrapper.update(); @@ -70,8 +70,8 @@ describe('AddPermissions', () => { name: 'ug1', }; - instance.typeChanged(evt); - instance.teamPicked(teamItem); + instance.onTypeChanged(evt); + instance.onTeamSelected(teamItem); wrapper.update(); diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index 4dcd07ffb48..289e27aa731 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -1,24 +1,19 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react'; import { observer } from 'mobx-react'; import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; -import UserPicker, { User } from 'app/core/components/Picker/UserPicker'; -import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker'; +import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; +import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; -export interface IProps { +export interface Props { permissions: any; - backendSrv: any; } + @observer -class AddPermissions extends Component { +class AddPermissions extends Component { constructor(props) { super(props); - this.userPicked = this.userPicked.bind(this); - this.teamPicked = this.teamPicked.bind(this); - this.permissionPicked = this.permissionPicked.bind(this); - this.typeChanged = this.typeChanged.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); } componentWillMount() { @@ -26,49 +21,49 @@ class AddPermissions extends Component { permissions.resetNewType(); } - typeChanged(evt) { + onTypeChanged = evt => { const { value } = evt.target; const { permissions } = this.props; permissions.setNewType(value); - } + }; - userPicked(user: User) { + onUserSelected = (user: User) => { const { permissions } = this.props; if (!user) { permissions.newItem.setUser(null, null); return; } return permissions.newItem.setUser(user.id, user.login, user.avatarUrl); - } + }; - teamPicked(team: Team) { + onTeamSelected = (team: Team) => { const { permissions } = this.props; if (!team) { permissions.newItem.setTeam(null, null); return; } return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl); - } + }; - permissionPicked(permission: OptionWithDescription) { + onPermissionChanged = (permission: OptionWithDescription) => { const { permissions } = this.props; return permissions.newItem.setPermission(permission.value); - } + }; resetNewType() { const { permissions } = this.props; return permissions.resetNewType(); } - handleSubmit(evt) { + onSubmit = evt => { evt.preventDefault(); const { permissions } = this.props; permissions.addStoreItem(); - } + }; render() { - const { permissions, backendSrv } = this.props; + const { permissions } = this.props; const newItem = permissions.newItem; const pickerClassName = 'width-20'; @@ -79,12 +74,12 @@ class AddPermissions extends Component { -
    -
    Add Permission For
    + +
    Add Permission For
    - {aclTypes.map((option, idx) => { return (
    - + {
    {}} + onSelected={() => {}} value={item.permission} disabled={true} className={'gf-form-input--form-dropdown-right'} diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx index b0158525d52..a17aa8c04df 100644 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ b/public/app/core/components/Permissions/PermissionsListItem.tsx @@ -68,7 +68,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
    void; + onSelected: (permission) => void; value: number; disabled: boolean; className?: string; @@ -16,14 +16,14 @@ export interface OptionWithDescription { description: string; } -class DescriptionPicker extends Component { +class DescriptionPicker extends Component { constructor(props) { super(props); this.state = {}; } render() { - const { optionsWithDesc, handlePicked, value, disabled, className } = this.props; + const { optionsWithDesc, onSelected, value, disabled, className } = this.props; return (
    @@ -34,7 +34,7 @@ class DescriptionPicker extends Component { clearable={false} labelKey="label" options={optionsWithDesc} - onChange={handlePicked} + onChange={onSelected} className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={DescriptionOption} placeholder="Choose" diff --git a/public/app/core/components/Picker/TeamPicker.jest.tsx b/public/app/core/components/Picker/TeamPicker.jest.tsx index 20b7620e0ac..3db9f7bb4eb 100644 --- a/public/app/core/components/Picker/TeamPicker.jest.tsx +++ b/public/app/core/components/Picker/TeamPicker.jest.tsx @@ -1,19 +1,23 @@ -import React from 'react'; +import React from 'react'; import renderer from 'react-test-renderer'; -import TeamPicker from './TeamPicker'; +import { TeamPicker } from './TeamPicker'; -const model = { - backendSrv: { - get: () => { - return new Promise((resolve, reject) => {}); - }, +jest.mock('app/core/services/backend_srv', () => ({ + getBackendSrv: () => { + return { + get: () => { + return Promise.resolve([]); + }, + }; }, - handlePicked: () => {}, -}; +})); describe('TeamPicker', () => { it('renders correctly', () => { - const tree = renderer.create().toJSON(); + const props = { + onSelected: () => {}, + }; + const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 2dfff1850dd..04f108ff8da 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -1,18 +1,19 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react'; import Select from 'react-select'; import PickerOption from './PickerOption'; -import withPicker from './withPicker'; import { debounce } from 'lodash'; +import { getBackendSrv } from 'app/core/services/backend_srv'; -export interface IProps { - backendSrv: any; - isLoading: boolean; - toggleLoading: any; - handlePicked: (user) => void; +export interface Props { + onSelected: (team: Team) => void; value?: string; className?: string; } +export interface State { + isLoading; +} + export interface Team { id: number; label: string; @@ -20,13 +21,12 @@ export interface Team { avatarUrl: string; } -class TeamPicker extends Component { +export class TeamPicker extends Component { debouncedSearch: any; - backendSrv: any; constructor(props) { super(props); - this.state = {}; + this.state = { isLoading: false }; this.search = this.search.bind(this); this.debouncedSearch = debounce(this.search, 300, { @@ -36,9 +36,9 @@ class TeamPicker extends Component { } search(query?: string) { - const { toggleLoading, backendSrv } = this.props; + const backendSrv = getBackendSrv(); + this.setState({ isLoading: true }); - toggleLoading(true); return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => { const teams = result.teams.map(team => { return { @@ -49,18 +49,18 @@ class TeamPicker extends Component { }; }); - toggleLoading(false); + this.setState({ isLoading: false }); return { options: teams }; }); } render() { - const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked, value, className } = this.props; + const { onSelected, value, className } = this.props; + const { isLoading } = this.state; return (
    - { loadOptions={this.debouncedSearch} loadingPlaceholder="Loading..." noResultsText="No teams found" - onChange={handlePicked} + onChange={onSelected} className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} - placeholder="Choose" + placeholder="Select a team" value={value} autosize={true} /> @@ -80,5 +80,3 @@ class TeamPicker extends Component { ); } } - -export default withPicker(TeamPicker); diff --git a/public/app/core/components/Picker/UserPicker.jest.tsx b/public/app/core/components/Picker/UserPicker.jest.tsx index 756fa2d9801..054ca643700 100644 --- a/public/app/core/components/Picker/UserPicker.jest.tsx +++ b/public/app/core/components/Picker/UserPicker.jest.tsx @@ -1,19 +1,20 @@ -import React from 'react'; +import React from 'react'; import renderer from 'react-test-renderer'; -import UserPicker from './UserPicker'; +import { UserPicker } from './UserPicker'; -const model = { - backendSrv: { - get: () => { - return new Promise((resolve, reject) => {}); - }, +jest.mock('app/core/services/backend_srv', () => ({ + getBackendSrv: () => { + return { + get: () => { + return Promise.resolve([]); + }, + }; }, - handlePicked: () => {}, -}; +})); describe('UserPicker', () => { it('renders correctly', () => { - const tree = renderer.create().toJSON(); + const tree = renderer.create( {}} />).toJSON(); expect(tree).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 77bf6c1fe15..e50513c44e1 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -1,18 +1,19 @@ import React, { Component } from 'react'; import Select from 'react-select'; import PickerOption from './PickerOption'; -import withPicker from './withPicker'; import { debounce } from 'lodash'; +import { getBackendSrv } from 'app/core/services/backend_srv'; -export interface IProps { - backendSrv: any; - isLoading: boolean; - toggleLoading: any; - handlePicked: (user) => void; +export interface Props { + onSelected: (user: User) => void; value?: string; className?: string; } +export interface State { + isLoading: boolean; +} + export interface User { id: number; label: string; @@ -20,13 +21,12 @@ export interface User { login: string; } -class UserPicker extends Component { +export class UserPicker extends Component { debouncedSearch: any; - backendSrv: any; constructor(props) { super(props); - this.state = {}; + this.state = { isLoading: false }; this.search = this.search.bind(this); this.debouncedSearch = debounce(this.search, 300, { @@ -36,29 +36,34 @@ class UserPicker extends Component { } search(query?: string) { - const { toggleLoading, backendSrv } = this.props; + const backendSrv = getBackendSrv(); - toggleLoading(true); - return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => { - const users = result.map(user => { + this.setState({ isLoading: true }); + + return backendSrv + .get(`/api/org/users?query=${query}&limit=10`) + .then(result => { return { - id: user.userId, - label: `${user.login} - ${user.email}`, - avatarUrl: user.avatarUrl, - login: user.login, + options: result.map(user => ({ + id: user.userId, + label: `${user.login} - ${user.email}`, + avatarUrl: user.avatarUrl, + login: user.login, + })), }; + }) + .finally(() => { + this.setState({ isLoading: false }); }); - toggleLoading(false); - return { options: users }; - }); } render() { - const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked, value, className } = this.props; + const { value, className } = this.props; + const { isLoading } = this.state; + return (
    - { loadOptions={this.debouncedSearch} loadingPlaceholder="Loading..." noResultsText="No users found" - onChange={handlePicked} + onChange={this.props.onSelected} className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} - placeholder="Choose" + placeholder="Select user" value={value} autosize={true} /> @@ -78,5 +83,3 @@ class UserPicker extends Component { ); } } - -export default withPicker(UserPicker); diff --git a/public/app/core/components/Picker/withPicker.tsx b/public/app/core/components/Picker/withPicker.tsx deleted file mode 100644 index 838ef927c30..00000000000 --- a/public/app/core/components/Picker/withPicker.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { Component } from 'react'; - -export interface IProps { - backendSrv: any; - handlePicked: (data) => void; - value?: string; - className?: string; -} - -export default function withPicker(WrappedComponent) { - return class WithPicker extends Component { - constructor(props) { - super(props); - this.toggleLoading = this.toggleLoading.bind(this); - - this.state = { - isLoading: false, - }; - } - - toggleLoading(isLoading) { - this.setState(prevState => { - return { - ...prevState, - isLoading: isLoading, - }; - }); - } - - render() { - return ; - } - }; -} diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index fd2e32db3a7..bd6b6975006 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -8,7 +8,7 @@ import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; export class GrafanaCtrl { @@ -24,6 +24,8 @@ export class GrafanaCtrl { backendSrv: BackendSrv, datasourceSrv: DatasourceSrv ) { + // sets singleston instances for angular services so react components can access them + setBackendSrv(backendSrv); createStore({ backendSrv, datasourceSrv }); $scope.init = function() { diff --git a/public/app/core/components/team_picker.ts b/public/app/core/components/team_picker.ts deleted file mode 100644 index 228767a76c4..00000000000 --- a/public/app/core/components/team_picker.ts +++ /dev/null @@ -1,64 +0,0 @@ -import coreModule from 'app/core/core_module'; -import _ from 'lodash'; - -const template = ` - -`; -export class TeamPickerCtrl { - group: any; - teamPicked: any; - debouncedSearchGroups: any; - - /** @ngInject */ - constructor(private backendSrv) { - this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, { - leading: true, - trailing: false, - }); - this.reset(); - } - - reset() { - this.group = { text: 'Choose', value: null }; - } - - searchGroups(query: string) { - return Promise.resolve( - this.backendSrv.get('/api/teams/search?perpage=10&page=1&query=' + query).then(result => { - return _.map(result.teams, ug => { - return { text: ug.name, value: ug }; - }); - }) - ); - } - - onChange(option) { - this.teamPicked({ $group: option.value }); - } -} - -export function teamPicker() { - return { - restrict: 'E', - template: template, - controller: TeamPickerCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - teamPicked: '&', - }, - link: function(scope, elem, attrs, ctrl) { - scope.$on('team-picker-reset', () => { - ctrl.reset(); - }); - }, - }; -} - -coreModule.directive('teamPicker', teamPicker); diff --git a/public/app/core/components/user_picker.ts b/public/app/core/components/user_picker.ts deleted file mode 100644 index 606ded09885..00000000000 --- a/public/app/core/components/user_picker.ts +++ /dev/null @@ -1,71 +0,0 @@ -import coreModule from 'app/core/core_module'; -import _ from 'lodash'; - -const template = ` - -`; -export class UserPickerCtrl { - user: any; - debouncedSearchUsers: any; - userPicked: any; - - /** @ngInject */ - constructor(private backendSrv) { - this.reset(); - this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, { - leading: true, - trailing: false, - }); - } - - searchUsers(query: string) { - return Promise.resolve( - this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => { - return _.map(result.users, user => { - return { text: user.login + ' - ' + user.email, value: user }; - }); - }) - ); - } - - onChange(option) { - this.userPicked({ $user: option.value }); - } - - reset() { - this.user = { text: 'Choose', value: null }; - } -} - -export interface User { - id: number; - name: string; - login: string; - email: string; -} - -export function userPicker() { - return { - restrict: 'E', - template: template, - controller: UserPickerCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - userPicked: '&', - }, - link: function(scope, elem, attrs, ctrl) { - scope.$on('user-picker-reset', () => { - ctrl.reset(); - }); - }, - }; -} - -coreModule.directive('userPicker', userPicker); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index fb7021fe883..d6088283f3b 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -44,8 +44,6 @@ import { KeybindingSrv } from './services/keybindingSrv'; import { helpModal } from './components/help/help'; import { JsonExplorer } from './components/json_explorer/json_explorer'; import { NavModelSrv, NavModel } from './nav_model_srv'; -import { userPicker } from './components/user_picker'; -import { teamPicker } from './components/team_picker'; import { geminiScrollbar } from './components/scroll/scroll'; import { pageScrollbar } from './components/scroll/page_scroll'; import { gfPageDirective } from './components/gf_page'; @@ -83,8 +81,6 @@ export { JsonExplorer, NavModelSrv, NavModel, - userPicker, - teamPicker, geminiScrollbar, pageScrollbar, gfPageDirective, diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index d582b6a3b18..1aeeedef4dd 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -368,3 +368,17 @@ export class BackendSrv { } coreModule.service('backendSrv', BackendSrv); + +// +// Code below is to expore the service to react components +// + +let singletonInstance: BackendSrv; + +export function setBackendSrv(instance: BackendSrv) { + singletonInstance = instance; +} + +export function getBackendSrv(): BackendSrv { + return singletonInstance; +} diff --git a/public/app/features/org/all.ts b/public/app/features/org/all.ts index 97e01c53fe3..8872450e3ab 100644 --- a/public/app/features/org/all.ts +++ b/public/app/features/org/all.ts @@ -5,8 +5,6 @@ import './select_org_ctrl'; import './change_password_ctrl'; import './new_org_ctrl'; import './user_invite_ctrl'; -import './teams_ctrl'; -import './team_details_ctrl'; import './create_team_ctrl'; import './org_api_keys_ctrl'; import './org_details_ctrl'; diff --git a/public/app/features/org/partials/team_details.html b/public/app/features/org/partials/team_details.html deleted file mode 100644 index 3ce851d5546..00000000000 --- a/public/app/features/org/partials/team_details.html +++ /dev/null @@ -1,105 +0,0 @@ - - -
    -

    Team Details

    - - -
    - Name - -
    -
    - - Email - - This is optional and is primarily used for allowing custom team avatars. - - - -
    - -
    - -
    - - -
    - -

    Team Members

    -
    -
    - Add member - - -
    -
    - - - - - - - - - - - - - - - - -
    UsernameEmail
    {{member.login}}{{member.email}} - - - -
    -
    - - This team has no members yet. - -
    - -
    - -
    - -

    Mappings to external groups

    -
    -
    - Add group - -
    -
    - -
    -
    - - - - - - - - - - - - -
    Group
    {{group.groupId}} - - - -
    -
    - - This team has no associated groups yet. - -
    - -
    diff --git a/public/app/features/org/partials/teams.html b/public/app/features/org/partials/teams.html deleted file mode 100755 index e15a15cf573..00000000000 --- a/public/app/features/org/partials/teams.html +++ /dev/null @@ -1,68 +0,0 @@ - - -
    -
    - -
    - - - - Add Team - -
    - -
    - - - - - - - - - - - - - - - - - - - -
    NameEmailMembers
    - - - -
    -
    - -
    -
      -
    1. - -
    2. -
    -
    - - - No Teams found. - -
    diff --git a/public/app/features/org/specs/team_details_ctrl.jest.ts b/public/app/features/org/specs/team_details_ctrl.jest.ts deleted file mode 100644 index c636de7ec56..00000000000 --- a/public/app/features/org/specs/team_details_ctrl.jest.ts +++ /dev/null @@ -1,42 +0,0 @@ -import '../team_details_ctrl'; -import TeamDetailsCtrl from '../team_details_ctrl'; - -describe('TeamDetailsCtrl', () => { - var backendSrv = { - searchUsers: jest.fn(() => Promise.resolve([])), - get: jest.fn(() => Promise.resolve([])), - post: jest.fn(() => Promise.resolve([])), - }; - - //Team id - var routeParams = { - id: 1, - }; - - var navModelSrv = { - getNav: jest.fn(), - }; - - var teamDetailsCtrl = new TeamDetailsCtrl({ $broadcast: jest.fn() }, backendSrv, routeParams, navModelSrv); - - describe('when user is chosen to be added to team', () => { - beforeEach(() => { - teamDetailsCtrl = new TeamDetailsCtrl({ $broadcast: jest.fn() }, backendSrv, routeParams, navModelSrv); - const userItem = { - id: 2, - login: 'user2', - }; - teamDetailsCtrl.userPicked(userItem); - }); - - it('should parse the result and save to db', () => { - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/teams/1/members'); - expect(backendSrv.post.mock.calls[0][1].userId).toBe(2); - }); - - it('should refresh the list after saving.', () => { - expect(backendSrv.get.mock.calls[0][0]).toBe('/api/teams/1'); - expect(backendSrv.get.mock.calls[1][0]).toBe('/api/teams/1/members'); - }); - }); -}); diff --git a/public/app/features/org/team_details_ctrl.ts b/public/app/features/org/team_details_ctrl.ts deleted file mode 100644 index 6e0fddafa9d..00000000000 --- a/public/app/features/org/team_details_ctrl.ts +++ /dev/null @@ -1,108 +0,0 @@ -import coreModule from 'app/core/core_module'; -import config from 'app/core/config'; - -export default class TeamDetailsCtrl { - team: Team; - teamMembers: User[] = []; - navModel: any; - teamGroups: TeamGroup[] = []; - newGroupId: string; - isMappingsEnabled: boolean; - - /** @ngInject **/ - constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) { - this.navModel = navModelSrv.getNav('cfg', 'teams', 0); - this.userPicked = this.userPicked.bind(this); - this.get = this.get.bind(this); - this.newGroupId = ''; - this.isMappingsEnabled = config.buildInfo.isEnterprise; - this.get(); - } - - get() { - if (this.$routeParams && this.$routeParams.id) { - this.backendSrv.get(`/api/teams/${this.$routeParams.id}`).then(result => { - this.team = result; - }); - - this.backendSrv.get(`/api/teams/${this.$routeParams.id}/members`).then(result => { - this.teamMembers = result; - }); - - if (this.isMappingsEnabled) { - this.backendSrv.get(`/api/teams/${this.$routeParams.id}/groups`).then(result => { - this.teamGroups = result; - }); - } - } - } - - removeTeamMember(teamMember: TeamMember) { - this.$scope.appEvent('confirm-modal', { - title: 'Remove Member', - text: 'Are you sure you want to remove ' + teamMember.login + ' from this group?', - yesText: 'Remove', - icon: 'fa-warning', - onConfirm: () => { - this.removeMemberConfirmed(teamMember); - }, - }); - } - - removeMemberConfirmed(teamMember: TeamMember) { - this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get); - } - - update() { - if (!this.$scope.teamDetailsForm.$valid) { - return; - } - - this.backendSrv.put('/api/teams/' + this.team.id, { - name: this.team.name, - email: this.team.email, - }); - } - - userPicked(user) { - this.backendSrv.post(`/api/teams/${this.$routeParams.id}/members`, { userId: user.id }).then(() => { - this.$scope.$broadcast('user-picker-reset'); - this.get(); - }); - } - - addGroup() { - this.backendSrv.post(`/api/teams/${this.$routeParams.id}/groups`, { groupId: this.newGroupId }).then(() => { - this.get(); - }); - } - - removeGroup(group: TeamGroup) { - this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/groups/${group.groupId}`).then(this.get); - } -} - -export interface TeamGroup { - groupId: string; -} - -export interface Team { - id: number; - name: string; - email: string; -} - -export interface User { - id: number; - name: string; - login: string; - email: string; -} - -export interface TeamMember { - userId: number; - name: string; - login: string; -} - -coreModule.controller('TeamDetailsCtrl', TeamDetailsCtrl); diff --git a/public/app/features/org/teams_ctrl.ts b/public/app/features/org/teams_ctrl.ts deleted file mode 100644 index 29317e73d3b..00000000000 --- a/public/app/features/org/teams_ctrl.ts +++ /dev/null @@ -1,66 +0,0 @@ -import coreModule from 'app/core/core_module'; -import appEvents from 'app/core/app_events'; - -export class TeamsCtrl { - teams: any; - pages = []; - perPage = 50; - page = 1; - totalPages: number; - showPaging = false; - query: any = ''; - navModel: any; - - /** @ngInject */ - constructor(private backendSrv, navModelSrv) { - this.navModel = navModelSrv.getNav('cfg', 'teams', 0); - this.get(); - } - - get() { - this.backendSrv - .get(`/api/teams/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`) - .then(result => { - this.teams = result.teams; - this.page = result.page; - this.perPage = result.perPage; - this.totalPages = Math.ceil(result.totalCount / result.perPage); - this.showPaging = this.totalPages > 1; - this.pages = []; - - for (var i = 1; i < this.totalPages + 1; i++) { - this.pages.push({ page: i, current: i === this.page }); - } - }); - } - - navigateToPage(page) { - this.page = page.page; - this.get(); - } - - deleteTeam(team) { - 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.backendSrv.delete('/api/teams/' + team.id).then(this.get.bind(this)); - } - - openTeamModal() { - appEvents.emit('show-modal', { - templateHtml: '', - modalClass: 'modal--narrow', - }); - } -} - -coreModule.controller('TeamsCtrl', TeamsCtrl); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 568b3438b38..cd1aed549e0 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,6 +5,8 @@ import ServerStats from 'app/containers/ServerStats/ServerStats'; import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; +import TeamPages from 'app/containers/Teams/TeamPages'; +import TeamList from 'app/containers/Teams/TeamList'; /** @ngInject **/ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -140,19 +142,23 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'OrgApiKeysCtrl', }) .when('/org/teams', { - templateUrl: 'public/app/features/org/partials/teams.html', - controller: 'TeamsCtrl', - controllerAs: 'ctrl', + template: '', + resolve: { + roles: () => ['Editor', 'Admin'], + component: () => TeamList, + }, }) .when('/org/teams/new', { templateUrl: 'public/app/features/org/partials/create_team.html', controller: 'CreateTeamCtrl', controllerAs: 'ctrl', }) - .when('/org/teams/edit/:id', { - templateUrl: 'public/app/features/org/partials/team_details.html', - controller: 'TeamDetailsCtrl', - controllerAs: 'ctrl', + .when('/org/teams/edit/:id/:page?', { + template: '', + resolve: { + roles: () => ['Admin'], + component: () => TeamPages, + }, }) .when('/profile', { templateUrl: 'public/app/features/org/partials/profile.html', diff --git a/public/app/stores/NavStore/NavItem.ts b/public/app/stores/NavStore/NavItem.ts index 4521d4291aa..3e8a2a837b3 100644 --- a/public/app/stores/NavStore/NavItem.ts +++ b/public/app/stores/NavStore/NavItem.ts @@ -1,4 +1,4 @@ -import { types } from 'mobx-state-tree'; +import { types } from 'mobx-state-tree'; export const NavItem = types.model('NavItem', { id: types.identifier(types.string), @@ -8,6 +8,7 @@ export const NavItem = types.model('NavItem', { icon: types.optional(types.string, ''), img: types.optional(types.string, ''), active: types.optional(types.boolean, false), + hideFromTabs: types.optional(types.boolean, false), breadcrumbs: types.optional(types.array(types.late(() => Breadcrumb)), []), children: types.optional(types.array(types.late(() => NavItem)), []), }); diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index 86348c00487..c69c32befa8 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; import { types, getEnv } from 'mobx-state-tree'; import { NavItem } from './NavItem'; +import { ITeam } from '../TeamsStore/TeamsStore'; export const NavStore = types .model('NavStore', { @@ -115,4 +116,43 @@ export const NavStore = types self.main = NavItem.create(main); }, + + initTeamPage(team: ITeam, tab: string, isSyncEnabled: boolean) { + let main = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: tab === 'members', + icon: 'gicon gicon-team', + id: 'team-members', + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: tab === 'settings', + icon: 'fa fa-fw fa-sliders', + id: 'team-settings', + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (isSyncEnabled) { + main.children.splice(1, 0, { + active: tab === 'groupsync', + icon: 'fa fa-fw fa-refresh', + id: 'team-settings', + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + self.main = NavItem.create(main); + }, })); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index c3bfe75d59c..8a915d20ef1 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -6,6 +6,7 @@ import { AlertListStore } from './../AlertListStore/AlertListStore'; import { ViewStore } from './../ViewStore/ViewStore'; import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; +import { TeamsStore } from './../TeamsStore/TeamsStore'; export const RootStore = types.model({ search: types.optional(SearchStore, { @@ -28,6 +29,9 @@ export const RootStore = types.model({ routeParams: {}, }), folder: types.optional(FolderStore, {}), + teams: types.optional(TeamsStore, { + map: {}, + }), }); type IRootStoreType = typeof RootStore.Type; diff --git a/public/app/stores/TeamsStore/TeamsStore.ts b/public/app/stores/TeamsStore/TeamsStore.ts new file mode 100644 index 00000000000..01cdca895d4 --- /dev/null +++ b/public/app/stores/TeamsStore/TeamsStore.ts @@ -0,0 +1,156 @@ +import { types, getEnv, flow } from 'mobx-state-tree'; + +export const TeamMember = types.model('TeamMember', { + userId: types.identifier(types.number), + teamId: types.number, + avatarUrl: types.string, + email: types.string, + login: types.string, +}); + +type TeamMemberType = typeof TeamMember.Type; +export interface ITeamMember extends TeamMemberType {} + +export const TeamGroup = types.model('TeamGroup', { + groupId: types.identifier(types.string), + teamId: types.number, +}); + +type TeamGroupType = typeof TeamGroup.Type; +export interface ITeamGroup extends TeamGroupType {} + +export const Team = types + .model('Team', { + id: types.identifier(types.number), + name: types.string, + avatarUrl: types.string, + email: types.string, + memberCount: types.number, + search: types.optional(types.string, ''), + members: types.optional(types.map(TeamMember), {}), + groups: types.optional(types.map(TeamGroup), {}), + }) + .views(self => ({ + get filteredMembers() { + let members = this.members.values(); + let regex = new RegExp(self.search, 'i'); + return members.filter(member => { + return regex.test(member.login) || regex.test(member.email); + }); + }, + })) + .actions(self => ({ + setName(name: string) { + self.name = name; + }, + + setEmail(email: string) { + self.email = email; + }, + + setSearchQuery(query: string) { + self.search = query; + }, + + update: flow(function* load() { + const backendSrv = getEnv(self).backendSrv; + + yield backendSrv.put(`/api/teams/${self.id}`, { + name: self.name, + email: self.email, + }); + }), + + loadMembers: flow(function* load() { + const backendSrv = getEnv(self).backendSrv; + const rsp = yield backendSrv.get(`/api/teams/${self.id}/members`); + self.members.clear(); + + for (let member of rsp) { + self.members.set(member.userId.toString(), TeamMember.create(member)); + } + }), + + removeMember: flow(function* load(member: ITeamMember) { + const backendSrv = getEnv(self).backendSrv; + yield backendSrv.delete(`/api/teams/${self.id}/members/${member.userId}`); + // remove from store map + self.members.delete(member.userId.toString()); + }), + + addMember: flow(function* load(userId: number) { + const backendSrv = getEnv(self).backendSrv; + yield backendSrv.post(`/api/teams/${self.id}/members`, { userId: userId }); + }), + + loadGroups: flow(function* load() { + const backendSrv = getEnv(self).backendSrv; + const rsp = yield backendSrv.get(`/api/teams/${self.id}/groups`); + self.groups.clear(); + + for (let group of rsp) { + self.groups.set(group.groupId, TeamGroup.create(group)); + } + }), + + addGroup: flow(function* load(groupId: string) { + const backendSrv = getEnv(self).backendSrv; + yield backendSrv.post(`/api/teams/${self.id}/groups`, { groupId: groupId }); + self.groups.set( + groupId, + TeamGroup.create({ + teamId: self.id, + groupId: groupId, + }) + ); + }), + + removeGroup: flow(function* load(groupId: string) { + const backendSrv = getEnv(self).backendSrv; + yield backendSrv.delete(`/api/teams/${self.id}/groups/${groupId}`); + self.groups.delete(groupId); + }), + })); + +type TeamType = typeof Team.Type; +export interface ITeam extends TeamType {} + +export const TeamsStore = types + .model('TeamsStore', { + map: types.map(Team), + search: types.optional(types.string, ''), + }) + .views(self => ({ + get filteredTeams() { + let teams = this.map.values(); + let regex = new RegExp(self.search, 'i'); + return teams.filter(team => { + return regex.test(team.name); + }); + }, + })) + .actions(self => ({ + loadTeams: flow(function* load() { + const backendSrv = getEnv(self).backendSrv; + const rsp = yield backendSrv.get('/api/teams/search/', { perpage: 50, page: 1 }); + self.map.clear(); + + for (let team of rsp.teams) { + self.map.set(team.id.toString(), Team.create(team)); + } + }), + + setSearchQuery(query: string) { + self.search = query; + }, + + loadById: flow(function* load(id: string) { + if (self.map.has(id)) { + return; + } + + const backendSrv = getEnv(self).backendSrv; + const team = yield backendSrv.get(`/api/teams/${id}`); + self.map.set(id, Team.create(team)); + }), + })); diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 756d88ee935..0de386f3f68 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -403,9 +403,9 @@ select.gf-form-input ~ .gf-form-help-icon { .cta-form { position: relative; - padding: 1rem; + padding: 1.5rem; background-color: $empty-list-cta-bg; - margin-bottom: 1rem; + margin-bottom: 2rem; border-top: 3px solid $green; } diff --git a/public/test/jest-shim.ts b/public/test/jest-shim.ts index 80c4bb3d21b..dbf9ac4be50 100644 --- a/public/test/jest-shim.ts +++ b/public/test/jest-shim.ts @@ -1,6 +1,17 @@ declare var global: NodeJS.Global; -(global).requestAnimationFrame = (callback) => { +(global).requestAnimationFrame = callback => { setTimeout(callback, 0); }; +(Promise.prototype).finally = function(onFinally) { + return this.then( + /* onFulfilled */ + res => Promise.resolve(onFinally()).then(() => res), + /* onRejected */ + err => + Promise.resolve(onFinally()).then(() => { + throw err; + }) + ); +}; From 9b50c9038b7697abc4e72191c18326af976f2dc8 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 12 Jul 2018 03:23:38 +0900 Subject: [PATCH 036/410] skip backend request if extended statistics is invalid. (#12495) * check extended statistics pattern * check extended statistics pattern * Revert "check extended statistics pattern" This reverts commit 52c7b1a972636d5f5729e64ae5e00e6fae329257. * add test * fix test --- .../datasource/cloudwatch/datasource.ts | 8 ++++++++ .../cloudwatch/specs/datasource.jest.ts | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 391f65bd7ae..00ce1bfa287 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -39,6 +39,14 @@ export default class CloudWatchDatasource { 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 + // valid ExtendedStatistics is like p90.00, check the pattern + let hasInvalidStatistics = item.statistics.some(s => { + return s.indexOf('p') === 0 && !/p\d{2}\.\d{2}/.test(s); + }); + if (hasInvalidStatistics) { + throw { message: 'Invalid extended statistics' }; + } + return _.extend( { refId: item.refId, diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts index 2dc6e57b1aa..a8968008661 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts @@ -121,6 +121,26 @@ describe('CloudWatchDatasource', function() { }); }); + it('should cancel query for invalid extended statistics', function () { + var query = { + range: { from: 'now-1h', to: 'now' }, + rangeRaw: { from: 1483228800, to: 1483232400 }, + targets: [ + { + region: 'us-east-1', + namespace: 'AWS/EC2', + metricName: 'CPUUtilization', + dimensions: { + InstanceId: 'i-12345678', + }, + statistics: ['pNN.NN'], + period: '60s', + }, + ], + }; + expect(ctx.ds.query.bind(ctx.ds, query)).toThrow(/Invalid extended statistics/); + }); + it('should return series list', function(done) { ctx.ds.query(query).then(function(result) { expect(result.data[0].target).toBe(response.results.A.series[0].name); From 81e62e105143f9493169d86a20bc2dd0766dab38 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 12 Jul 2018 13:16:41 +0200 Subject: [PATCH 037/410] 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 Date: Thu, 12 Jul 2018 15:38:41 +0200 Subject: [PATCH 038/410] 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 Date: Fri, 13 Jul 2018 02:56:37 -0400 Subject: [PATCH 039/410] 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 Date: Fri, 13 Jul 2018 09:09:36 +0200 Subject: [PATCH 040/410] 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 { - 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 { } 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 { 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 { }; 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 { 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 (
    @@ -264,6 +308,18 @@ export class Explore extends React.Component {
    )} + {!datasourceMissing ? ( +
    + + +
    From ae935bf08b14c1457b4f96580048003c494b8063 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 23 Jul 2018 11:06:30 +0200 Subject: [PATCH 078/410] 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 = {}; + + 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?= Date: Mon, 23 Jul 2018 12:07:54 +0200 Subject: [PATCH 079/410] 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 Date: Mon, 23 Jul 2018 13:13:18 +0200 Subject: [PATCH 080/410] 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 Date: Mon, 23 Jul 2018 13:38:16 +0200 Subject: [PATCH 081/410] 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 = {}; 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 Date: Mon, 23 Jul 2018 13:39:32 +0200 Subject: [PATCH 082/410] 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 Date: Mon, 23 Jul 2018 14:28:17 +0200 Subject: [PATCH 083/410] 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 = { + 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 Date: Thu, 12 Jul 2018 15:32:32 +0200 Subject: [PATCH 084/410] 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 Date: Mon, 23 Jul 2018 14:54:58 +0200 Subject: [PATCH 085/410] 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 Date: Mon, 23 Jul 2018 14:58:11 +0200 Subject: [PATCH 086/410] 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 Date: Mon, 23 Jul 2018 15:25:59 +0200 Subject: [PATCH 087/410] 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 Date: Mon, 23 Jul 2018 15:34:03 +0200 Subject: [PATCH 088/410] 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 Date: Mon, 23 Jul 2018 15:42:47 +0200 Subject: [PATCH 089/410] 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 Date: Mon, 23 Jul 2018 19:53:26 +0530 Subject: [PATCH 090/410] 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 Date: Tue, 24 Jul 2018 16:58:48 +0900 Subject: [PATCH 091/410] 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 Date: Tue, 24 Jul 2018 10:34:11 +0200 Subject: [PATCH 092/410] 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 Date: Tue, 24 Jul 2018 11:03:46 +0200 Subject: [PATCH 093/410] 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 Date: Tue, 24 Jul 2018 11:26:09 +0200 Subject: [PATCH 094/410] 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 Date: Tue, 24 Jul 2018 11:27:53 +0200 Subject: [PATCH 095/410] 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 = { - 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 Date: Tue, 24 Jul 2018 14:34:37 +0200 Subject: [PATCH 096/410] 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 = {}; - let ctx = { - 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 Date: Tue, 24 Jul 2018 14:35:37 +0200 Subject: [PATCH 097/410] 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 Date: Fri, 29 Jun 2018 13:37:21 +0200 Subject: [PATCH 098/410] 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 = { - performInstantQuery: sinon - .stub() - .withArgs({ expr: '{__name__="node_cpu"' }) - .returns( - Promise.resolve({ - data: { + + let 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 Date: Fri, 29 Jun 2018 13:44:11 +0200 Subject: [PATCH 099/410] 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 = {} - let datasourceStub = new PrometheusDatasource({},{},backendSrv,{},{}); + let 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 Date: Mon, 2 Jul 2018 09:43:34 +0200 Subject: [PATCH 100/410] 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 Date: Mon, 2 Jul 2018 14:34:58 +0200 Subject: [PATCH 101/410] 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 Date: Tue, 24 Jul 2018 14:55:54 +0200 Subject: [PATCH 102/410] 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 Date: Tue, 24 Jul 2018 15:24:44 +0200 Subject: [PATCH 103/410] 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 Date: Tue, 24 Jul 2018 15:27:13 +0200 Subject: [PATCH 104/410] 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 Date: Wed, 4 Jul 2018 09:22:39 +0200 Subject: [PATCH 105/410] 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 Date: Tue, 24 Jul 2018 15:39:56 +0200 Subject: [PATCH 106/410] 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 = { + 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 Date: Tue, 24 Jul 2018 15:51:34 +0200 Subject: [PATCH 107/410] 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 Date: Tue, 24 Jul 2018 15:57:07 +0200 Subject: [PATCH 108/410] 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 Date: Tue, 24 Jul 2018 16:45:36 +0200 Subject: [PATCH 109/410] 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 Date: Tue, 24 Jul 2018 17:03:58 +0200 Subject: [PATCH 110/410] 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 Date: Tue, 24 Jul 2018 17:40:00 +0200 Subject: [PATCH 111/410] 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 Date: Tue, 24 Jul 2018 18:30:29 +0200 Subject: [PATCH 112/410] 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 Date: Tue, 24 Jul 2018 19:05:09 +0200 Subject: [PATCH 113/410] 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 Date: Tue, 24 Jul 2018 19:21:23 +0200 Subject: [PATCH 114/410] 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 Date: Wed, 25 Jul 2018 11:27:43 +0900 Subject: [PATCH 115/410] 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 Date: Wed, 25 Jul 2018 10:29:55 +0200 Subject: [PATCH 116/410] 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 { {team.memberCount} - this.deleteTeam(team)} /> + this.deleteTeam(team)} /> ); 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( 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 { - state = { - deleteButton: 'delete-button--show', - confirmSpan: 'confirm-delete--removed', +export interface DeleteButtonProps { + onConfirmDelete(); +} + +export interface DeleteButtonStates { + showConfirm: boolean; +} + +export default class DeleteButton extends PureComponent { + 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 ( - + - - + + Cancel - + Confirm Delete 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 Date: Wed, 25 Jul 2018 11:27:43 +0200 Subject: [PATCH 117/410] 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 Date: Wed, 25 Jul 2018 11:54:51 +0200 Subject: [PATCH 118/410] 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 Date: Wed, 25 Jul 2018 11:55:34 +0200 Subject: [PATCH 119/410] 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 Date: Wed, 25 Jul 2018 13:22:55 +0200 Subject: [PATCH 120/410] 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 Date: Wed, 25 Jul 2018 14:14:25 +0200 Subject: [PATCH 121/410] 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 Date: Wed, 25 Jul 2018 14:52:03 +0200 Subject: [PATCH 122/410] 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 = { + 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 Date: Wed, 25 Jul 2018 16:15:03 +0200 Subject: [PATCH 123/410] 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 = {}; 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 Date: Wed, 25 Jul 2018 16:20:00 +0200 Subject: [PATCH 124/410] 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([$__range]) != ) +Regex: +``` ### Using variables in queries From 84e431d377b51405f37b4bae8321454218bcc7c4 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 25 Jul 2018 16:16:33 +0200 Subject: [PATCH 125/410] 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 Date: Wed, 25 Jul 2018 17:38:45 +0200 Subject: [PATCH 126/410] 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 = {}; 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 Date: Thu, 26 Jul 2018 09:36:46 +0200 Subject: [PATCH 127/410] 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 = {}; let $rootscope = { $on: () => {}, }; - let ctx = { - datasourceSrv: {}, - $location: {}, - dashboard: {}, - }; + let ctx = {}; 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 Date: Thu, 26 Jul 2018 10:44:40 +0200 Subject: [PATCH 128/410] 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 = {}; + 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 Date: Thu, 26 Jul 2018 14:04:12 +0200 Subject: [PATCH 129/410] 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().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().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( + + ).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( + + ).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().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( + + ).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( + + ).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( + + ).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( + + ).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 { + 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 = 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 = 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 ( + + ); + } +} + +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 { - 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 { - 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 { + 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 { } 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 { 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 { } 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 { }); }; - 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 { } }; - 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 { // 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 { 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 ( @@ -591,4 +488,24 @@ class QueryField extends React.Component { } } +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 { constructor(props) { @@ -62,9 +61,6 @@ class QueryRow extends PureComponent { portalPrefix="explore" onPressEnter={this.handlePressEnter} onQueryChange={this.handleChangeQuery} - placeholder="Enter a PromQL query" - prismLanguage="promql" - prismDefinition={promql} request={request} />
    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 { - el: any; +interface TypeaheadItemProps { + isSelected: boolean; + item: Suggestion; + onClickItem: (Suggestion) => void; +} + +class TypeaheadItem extends React.PureComponent { + el: HTMLElement; + componentDidUpdate(prevProps) { if (this.props.isSelected && !prevProps.isSelected) { scrollIntoView(this.el); @@ -22,20 +31,30 @@ class TypeaheadItem extends React.PureComponent { 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 ( -
  • - {label} - {hint && isSelected ?
    {hint}
    : null} +
  • + {item.detail || item.label} + {item.documentation && isSelected ?
    {item.documentation}
    : null}
  • ); } } -class TypeaheadGroup extends React.PureComponent { +interface TypeaheadGroupProps { + items: Suggestion[]; + label: string; + onClickItem: (Suggestion) => void; + selected: Suggestion; +} + +class TypeaheadGroup extends React.PureComponent { render() { const { items, label, selected, onClickItem } = this.props; return ( @@ -43,16 +62,8 @@ class TypeaheadGroup extends React.PureComponent {
    {label}
      {items.map(item => { - const text = typeof item === 'object' ? item.text : item; - const label = typeof item === 'object' ? item.display || item.text : item; return ( - -1} - hint={item.hint} - label={label} - /> + ); })}
    @@ -61,13 +72,19 @@ class TypeaheadGroup extends React.PureComponent { } } -class Typeahead extends React.PureComponent { +interface TypeaheadProps { + groupedItems: SuggestionGroup[]; + menuRef: any; + selectedItem: Suggestion | null; + onClickItem: (Suggestion) => void; +} +class Typeahead extends React.PureComponent { render() { - const { groupedItems, menuRef, selectedItems, onClickItem } = this.props; + const { groupedItems, menuRef, selectedItem, onClickItem } = this.props; return (
      {groupedItems.map(g => ( - + ))}
    ); 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 Date: Thu, 26 Jul 2018 17:22:15 +0200 Subject: [PATCH 130/410] 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 = {}; 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 Date: Thu, 26 Jul 2018 18:09:42 +0200 Subject: [PATCH 131/410] 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 Date: Thu, 26 Jul 2018 18:10:17 +0200 Subject: [PATCH 132/410] 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 Date: Thu, 26 Jul 2018 18:10:45 +0200 Subject: [PATCH 133/410] 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 Date: Thu, 26 Jul 2018 18:11:10 +0200 Subject: [PATCH 134/410] 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 Date: Thu, 26 Jul 2018 18:12:00 +0200 Subject: [PATCH 135/410] 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 Date: Thu, 26 Jul 2018 21:39:02 +0200 Subject: [PATCH 136/410] 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, "", 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 Date: Fri, 27 Jul 2018 10:33:06 +0200 Subject: [PATCH 137/410] 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 Date: Fri, 27 Jul 2018 11:04:01 +0200 Subject: [PATCH 138/410] 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 Date: Fri, 27 Jul 2018 11:28:16 +0200 Subject: [PATCH 139/410] 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 Date: Fri, 27 Jul 2018 11:30:37 +0200 Subject: [PATCH 140/410] 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 Date: Fri, 27 Jul 2018 11:34:14 +0200 Subject: [PATCH 141/410] 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 Date: Fri, 27 Jul 2018 13:21:40 +0200 Subject: [PATCH 142/410] 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 Date: Fri, 27 Jul 2018 13:29:57 +0200 Subject: [PATCH 143/410] 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 Date: Fri, 27 Jul 2018 13:33:50 +0200 Subject: [PATCH 144/410] 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 Date: Fri, 27 Jul 2018 13:41:07 +0200 Subject: [PATCH 145/410] 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 Date: Fri, 27 Jul 2018 13:45:16 +0200 Subject: [PATCH 146/410] 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 @@

    User Profile

    -
    + -
    - Name - -
    -
    - Email - +
    + Name + +
    +
    + Email +
    -
    - Username +
    + Username
    -
    - -
    - +
    + +
    + - + -

    Organizations

    +

    Teams

    +
    + + + + + + + + + + + + + +
    NameEmail
    {{team.name}}{{team.email}}
    +
    + +

    Organizations

    - - - - - - - - - - - - - - - -
    NameRole
    {{org.name}}{{org.role}} - - Current - - - Select - -
    -
    - + + + + + + + + + + + + + + + +
    NameRole
    {{org.name}}{{org.role}} + + Current + + + Select + +
    +
    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 Date: Fri, 27 Jul 2018 13:48:14 +0200 Subject: [PATCH 147/410] 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 {
    -
    +
    From 4e6168f3a331e5701e279305774413eca87499d4 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Fri, 27 Jul 2018 14:22:48 +0200 Subject: [PATCH 148/410] 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 Date: Fri, 27 Jul 2018 14:40:56 +0200 Subject: [PATCH 149/410] 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 Date: Fri, 27 Jul 2018 15:16:19 +0200 Subject: [PATCH 150/410] 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 Date: Sat, 21 Jul 2018 11:04:05 +0200 Subject: [PATCH 151/410] 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 Date: Tue, 24 Jul 2018 18:31:47 +0200 Subject: [PATCH 152/410] 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 Date: Tue, 24 Jul 2018 19:25:48 +0200 Subject: [PATCH 153/410] 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 @@
    Time series:
     - return column named time (UTC in seconds or timestamp)
     - return column(s) with numeric datatype as values
    -- (Optional: return column named metric 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 metric 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 
    Date: Fri, 27 Jul 2018 15:51:56 +0200
    Subject: [PATCH 154/410] 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 
    Date: Fri, 27 Jul 2018 15:52:22 +0200
    Subject: [PATCH 155/410] 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 156/410] 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 
    Date: Fri, 27 Jul 2018 16:45:03 +0200
    Subject: [PATCH 157/410] 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 = {};
     
    -  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 
    Date: Fri, 27 Jul 2018 16:46:41 +0200
    Subject: [PATCH 158/410] 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 = {};
     
       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 
    Date: Fri, 27 Jul 2018 17:14:27 +0200
    Subject: [PATCH 159/410] 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 
    Date: Fri, 27 Jul 2018 17:18:45 +0200
    Subject: [PATCH 160/410] 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 @@
     	
    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 metric 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 @@
     		
    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 metric 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 metric 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 
    Date: Fri, 27 Jul 2018 17:54:51 +0200
    Subject: [PATCH 161/410] 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 
    Date: Fri, 27 Jul 2018 18:13:19 +0200
    Subject: [PATCH 162/410] 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 
    Date: Sat, 28 Jul 2018 11:31:30 +0200
    Subject: [PATCH 163/410] 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 @@
    +
    +
    +
    +
    +  
    +    
    +    
    +      
    +      
    +    
    +    
    +    
    +      
    +      
    +    
    +    
    +    
    +      
    +      
    +    
    +  
    +  
    +    
    +      
    +        image/svg+xml
    +        
    +        
    +      
    +    
    +  
    +  
    +    
    +      
    +        
    +        
    +        
    +      
    +    
    +  
    +
    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 
    Date: Mon, 30 Jul 2018 08:02:16 +0100
    Subject: [PATCH 164/410] 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 
    Date: Mon, 30 Jul 2018 10:19:51 +0200
    Subject: [PATCH 165/410] 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 
    Date: Mon, 30 Jul 2018 10:21:22 +0200
    Subject: [PATCH 166/410] 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 
    Date: Mon, 30 Jul 2018 13:02:08 +0200
    Subject: [PATCH 167/410] 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 = {};
    +let instanceSettings = {
    +  url: 'proxied',
    +  directUrl: 'direct',
    +  user: 'test',
    +  password: 'mupp',
    +  jsonData: { httpMethod: 'GET' },
    +};
    +let backendSrv = {
    +  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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 
    Date: Mon, 30 Jul 2018 13:50:18 +0200
    Subject: [PATCH 168/410] 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 
    Date: Mon, 30 Jul 2018 13:50:52 +0200
    Subject: [PATCH 169/410] 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 
    Date: Mon, 30 Jul 2018 13:54:57 +0200
    Subject: [PATCH 170/410] 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 
    Date: Mon, 30 Jul 2018 14:37:23 +0200
    Subject: [PATCH 171/410] 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 172/410] 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 173/410] 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 
    Date: Mon, 30 Jul 2018 15:51:15 +0100
    Subject: [PATCH 174/410] 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 
    Date: Mon, 30 Jul 2018 17:03:01 +0200
    Subject: [PATCH 175/410] 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 
    Date: Mon, 30 Jul 2018 17:03:24 +0200
    Subject: [PATCH 176/410] 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 
    Date: Tue, 31 Jul 2018 09:35:08 +0200
    Subject: [PATCH 177/410] 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 = {
     };
     
     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, 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 
    Date: Tue, 31 Jul 2018 11:41:58 +0200
    Subject: [PATCH 178/410] 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 
    Date: Tue, 31 Jul 2018 12:51:07 +0200
    Subject: [PATCH 179/410] 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 += `
    -        
    +        
               ${value}
             
           `;
    
    From 4b8ec4e32330b9fd606acc39604a8b9de7229ac3 Mon Sep 17 00:00:00 2001
    From: Patrick O'Carroll 
    Date: Tue, 31 Jul 2018 13:07:43 +0200
    Subject: [PATCH 180/410] 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 += `
    -        
    +        
               ${value}
             
           `;
    
    From 276a5e6eb5603df07d48aa66af4763bc9f3576c8 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Torkel=20=C3=96degaard?= 
    Date: Tue, 31 Jul 2018 17:29:02 +0200
    Subject: [PATCH 181/410] 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?= 
    Date: Tue, 31 Jul 2018 17:31:45 +0200
    Subject: [PATCH 182/410] 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.children}
           {props.tooltip && (
    -        
    +        
               
             
           )}
    
    From 6df3722a35faf455e2d25989a80a8e167531b5b7 Mon Sep 17 00:00:00 2001
    From: Marcus Efraimsson 
    Date: Tue, 31 Jul 2018 18:01:36 +0200
    Subject: [PATCH 183/410] 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 
    Date: Mon, 30 Jul 2018 13:23:29 +0200
    Subject: [PATCH 184/410] 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 -> extract(epoch from column) as "time"
     - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
     - $__unixEpochFilter(column) ->  column >= 1492750877 AND column <= 1492750877
    -- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS time
    +- $__timeGroup(column,'5m') -> (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 
    Date: Wed, 1 Aug 2018 08:00:43 +0200
    Subject: [PATCH 185/410] 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 
    Date: Wed, 1 Aug 2018 08:48:22 +0200
    Subject: [PATCH 186/410] 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 
    Date: Wed, 1 Aug 2018 09:08:17 +0200
    Subject: [PATCH 187/410] 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 += `
    -        
    +        
               ${value}
             
           `;
    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 = `
             
    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 Date: Wed, 1 Aug 2018 09:30:26 +0200 Subject: [PATCH 188/410] 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 Date: Wed, 1 Aug 2018 10:09:05 +0200 Subject: [PATCH 189/410] 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 Date: Wed, 1 Aug 2018 12:30:50 +0200 Subject: [PATCH 190/410] 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 Date: Wed, 1 Aug 2018 14:26:29 +0200 Subject: [PATCH 191/410] 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 = {}; -let instanceSettings = { - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'GET' }, -}; -let backendSrv = { - 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 = {}; +let instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'GET' }, +}; +let backendSrv = { + 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 Date: Wed, 1 Aug 2018 14:27:45 +0200 Subject: [PATCH 192/410] 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 Date: Wed, 1 Aug 2018 15:06:18 +0200 Subject: [PATCH 193/410] 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 Date: Wed, 1 Aug 2018 19:38:13 +0200 Subject: [PATCH 194/410] 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 Date: Wed, 1 Aug 2018 20:58:51 +0200 Subject: [PATCH 195/410] 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 Date: Wed, 1 Aug 2018 21:23:00 +0200 Subject: [PATCH 196/410] 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.
    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) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. +- $__timeGroupAlias(column, '5m'[, fillvalue]) -> 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) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 - $__timeGroup(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) +- $__timeGroupAlias(column,'5m') -> 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) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 +- $__timeGroupAlias(column,'5m') -> (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 Date: Thu, 2 Aug 2018 11:18:21 +0200 Subject: [PATCH 197/410] 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 += ` - + ${value} `; 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 = `
    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 Date: Thu, 2 Aug 2018 11:21:17 +0200 Subject: [PATCH 198/410] 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 Date: Thu, 2 Aug 2018 11:43:48 +0200 Subject: [PATCH 199/410] 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 Date: Thu, 2 Aug 2018 18:49:40 +0900 Subject: [PATCH 200/410] 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?= Date: Thu, 2 Aug 2018 11:51:41 +0200 Subject: [PATCH 201/410] 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 Date: Thu, 2 Aug 2018 12:37:50 +0200 Subject: [PATCH 202/410] 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?fQ1jZL)T|DBuDfGX0GIKe# zZ(v|PG+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)Bt18m`s-h3F39V-T*GMSD%k6~jU_LU2Zza=(?3EWNE@953{ha0>cQz%} zKmT!&x5w)RXEu?u$Pj5UlKI^} 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!#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)XAB!-La2MU7o566!t+ z7_YE|&;=)1iK`>2gZLYrDGc?_jN}mdyoAfQ8Vsz^uD_zh4dK56hb}q29bb> zVt59S^1+=Yk?grHCZVpheBMsm1N;mRjOEkj%OT<~;qRYTqk}F5A%jNvZOmZtyb>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$-nmk zv;^!P6i;XuG9ZrPP|$nE0yr1AOAH=L5K^L+WyP1WiLm&M(n|eubv5nXNwuIYJsvnXD_rq?AZB1@(~(o7aEqn_+gLrVogAl$l2gl}V4>BB@=Le3iwG&O8LYw}AC~-O{pwKBpOm0t zt^nrOUfA$-vOCIL8>sw=V{u%9JjmgN$qOm*b~%4-k64<3K&m3 zMbd1%d&5ni6DtlEDvU5AG8c|LJdW{FpAck{W_~$v8*%HM9~rS4bE5}=8N2&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*(zayejFGNPqhw2b+>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+%NXRIwgST zeeRDJrJ z!j9N%JHtaX36v8_KMBn~4ZmZ|V{Hz@O!%#Lo^k-Qv$*Rgm2`eyR7SwG?ay<0+IeHH z2E?GrA_lQeI6k+nJMFvaxiI62af$}YojO*sqN_igU}cm4`q|F7^hOn zfJ_7X+T3oKuRs4OGP{++xb6o`U5Rhnbaq~kR3sK&0E5vtIQW0ITLuOQgw#HZYubRK}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&49AzadyYNp;7P2@l*J&VVRKYKtmeA$AF=>cnhHGInXu32b);5t9F<@WqSV zH5UVO6<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 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)IluCLv*mqRhj<$nJlKf)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!0HjdriMldN1BF1c_fo|Gq?%-q>0aiH)I zL9Jdu>_q@&H%S1;zR30P5h0sVrKk-!M>uAO;H_q{3MG!#o~hEi)fbayX+?K-K_8Ks zsC+vAuorc1&+cP%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-ib1o9i(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?#!M5VUmufQ!HBkPCjkd?w;JM2wwiO=^Oe8t&|p1% zx7x+W$3k)P8jBw)?vp_4|4A;*`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_nFh1fYl@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&{{Nixy%)6-}Bik-^K?+#d>w8*1!Pg*PR(#ZG)&X=QWgY26w z;sJ=6C3}03hTe7(5v_w3PI7BI)CGvz7f<`KOWxlcniV~aTH>b8+nL?~ z5z$nzM!RO#FsR?uY-Dm_<($&zZHSjEf6Skz;!3*iLKg9-ov^#e25n@aY`Q6T7D(U< z6(C#^@mV3FT_y%kG*!q zDtjc>R+)fVnj)>HWV50H4wzpQopI9-f7i15-F~;f4=$2Ge)Ae^8>9>vQbBT$zQo=oPyvAWisI9n*T{TzIBnRD9*_*AO4bOTUZ4<#~ zR>NC!RC~xU8=^u~7{YGK0%ZI%f;v3cKj|JyIR(KD&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|%^!F=pN~8r@h~5iid|kMEe4^nptj3 zL3?lVH!lq57AzK1>bO+`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}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-!oLShA+Xjh2nzbl&&3*Svjvx%y zshn 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` zHHz63yLUbZ617*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-vWAkci9E8x%;gsqhk{A&uNTp#?9)V@*@mYu{FkAa~=yj&0jj zs{fGavG1Dcv79Pb@muZyAX9y*1Me1lnUr(|O#T*elOJK%Q zZ?DMwgF*>r^@}{WCp)=+E%JCONQZ9Sk>+H)wDE1lpV0A6#S*<8#kIKB6e6d2_kt;= z7{)!ZeMl>dTPfWtRK~*F0b#7It>v;VO 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-Np~E)Z;aIt_2Ru2kG7*qLS6DTs1| z7{1fUu)XK(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_2np8pZhp=&PZvmwir?&i}QiyT0C0t$%R16q{XdTs{=YZt-4wr7z z^YG)P5|PKD1ob6@;!*-+M7x?vDY}OvRIdb%yaYaS%=(IrHQxecd-;+qF(@ufpA@Ou2krVdSO zuTX1iZEs}Ng|#X19VQt^u5MeDwBMc)Xg}<0icbztI+7aK(TA@$_{(ES%c~%ja4wBE zVzLN@R-V60N6U5$(8BuNCE!Pvwl9Ql9miDTn!RyCGC!sHAyB)I3dpe zgPb{ApAjvrLOH{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$8OY{*k9R=s3O6e2Z|=t(o+NvV;U>Bj zEPU*g>|Hj<*KmAr+G&PRyXNAL0t_ZRE;R{`hRP{~J@#H2scg%z4VB zXkHGo2VK4w^EA>n6Zru_6UyS1c0B;h$AM-Q_rSXJVka=xX$@+1Q~CbRhJ1FsAp=95?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?3^b z96qG{fHr|SxYPY_4+hyl(LF;keeO=wMIgw8`-W--VTRN40p+&@p*6diiV3MnwJQ;4 z5OGEh!p0VlvPg@u5TOl?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(8WCHYvZo10@~x%U#v4$I64#3g^jIEU04vFml> zx17)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-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+PrvwLOz`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 zE9A7UY5mEIjG#@;YS6sC_%V32%<$XHq||jOqB> zP&Z2LIOD^Y5!CQxp5)rPBGm_49WWOZ1*H>uf9b5Jz}T%&JZ=Kk01RU6TqSlqTypSl zX>07Z+Ut56Gd3*%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|N_7Xyow;8?jBU+|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<`-uiGU_%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#GR2C)~jP#T?~j4mReDeii|&=_^g zp#(oYV-+^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*`r7o4yZy*)Z^kZ-+g1JD3izR_uS~?_&g!6F) zK$?o<8X{jWmMDTBBly+awmNv&cDiexL+>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_(BImt+s)I(bm+d)CkrglfF z1#>>cNkz(KyI=}`bLhYOExLeb=~mCk7Q1G$bA=<(07HApd7<%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 + + + + + #2b5797 + + + 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;|Qdzxdz27wtc-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_44P9hzr`%?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)&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?

    9TcXe4aBrtVUSe8<)-#McI2T z3?hED}e(iS*W4bD=>JQ(0e{Ysja;1x;M*6=h;_}H#+#sx(aLD_ z-_17r4l#pcww0_p-+(2aD8nj77sz!7lc7W5@e>ie<(bq4`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>hil>fCQ$#xH(7u29kxTylAe-3!PA#&P=S_>T_r)f?}U3DWqEd1X}{eqz@nFvSEvV`rQ14<;HaUme$hO zkBye;n9E#zM_yG!RyB!?O$G(1 zSzl_&>krK*r4U9d#RVOEV^^QM`SySEI8{tZSCA9=LNjW>hHt7A54KEuJ<5%x`X)Q$~KZjk!P92-U3BjhGGdH93_)gTC zGoI~9r}?28{C({kR9s)*fmSFdn#@9_Sp6Y#dwZrAc6&Q*aS`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(lfuJ)|3?@c<;wYtpFK)foMwF3;vS`I&sQsQ zaXuNNkjEk0rCT$AoME^y_W!eT2R7K(XFMR?5azGf{^KcpSZDnj7q6hdnaf2K|M1c=^8EfAmm;HVk0T2Y0OX23jNY}`Rb8)A%qVUg`NjJ(M4P^ zq!2+VgI;L6jD=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{HnjDq#}k#sXuVAe=+jmDm$N94gbu#?>O6h@E}<% z!*pmZs*A4p3ZVkmc3u6+f0R$Cm6BH9Y|j7U@0*SwzdrM0jz_PMJ<{~93unnz%N24x zb8#OGQo~4Oe5*NCQQTCE8*Xpio>0ZqgCp%uTbrew%B@xPdGTQ1I@cxZo;%&S%$ zv+9m*wK(K!-i%thAa4IpMoPwYosfa-S;Ll@mFMGw~1eGxTw|)6Yrt(ZT6|@W~!Q93Q^#-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@)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)^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{BP}Zmwku{!EI0vdirxV6prf%O+Do;UUR6u;5Dd{hEe;+_(VvU z=a$pPb1lddj$UM)InSp4-iApTU9zaT`GHj`3r{*HQ*`>`$|QV$eKMMk3xbM^Eh1}QN(Y9GIQ>i1Qb@EXWWpA>A{mbI}lCL4{|m%K>xR~E$mNKqe8-)iQ_YI1j6hHL=}r;1 zBRXCwK;NhGckm1fxjx%TNd~g?1CK}oP#T$&&J?Q;1rAxfqbom#NlB_oo8L=`l+}fd?WD?mO&Ws;4^BVDM1TwnvqmBI&_<>vy-qy_o_3!w^41s zu7qY6ZJYbO(*P?xAz7ExVHaXF07>HCXQ9rj3JHsGCGe;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=RVeouX!uT}4CKFEmpC3_~%Bpp8ok1XJf;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}u+SETpZ4n^5tAQ;DR7i6YDVBNi{yOH)?2UflZqJxvgUw;t=wduXgpVhsj*uiR>0 zoXfW0l%0OKYf+jsQ9JD(`!FvXp_&E`OWZqQ3vshLeQR1gY!4$9K?E1l36g2y-bKv= z%d6&%h;R)SR_S38g+&y$Xy~*vcWTQ(m+90ZGIsaE<)Co;U;WjTn+DBvK7BoU7!jweV9C#PX|Aw;q~ zs0{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;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>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$OtpOAt0V>*|co*Wc+er7eJvB$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&5CHU4@&!o`Ka*k3R86c9|tbI+czJLK?Ettr630m zfPVa*-h{VA^*H>&kE3j_ElAe5ZkrP){*gOHKo@*if>ur_rjh!v6Hav z5WI6S0~55K2*t#Jd#|5xqgbhqsoVf4uaR3siT2fb+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%_)_pQ<+rM?f$*Mggg1WQaFfnhI_M8_8AqrA&!G;$!*co%RT}fqCe+GBQGis zSrFK9_2_RCIANc12-#E#ws;s_g1tp?zBkX7PIF$<$Sz2sz0SCtehdmk4S~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&

    ' + cellHtml + ''; + if (rowClasses.length) { + rowClass = ' class="' + rowClasses.join(' ') + '"'; + } + + html += '' + cellHtml + ''; } 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 Date: Thu, 2 Aug 2018 15:49:50 +0200 Subject: [PATCH 208/410] 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 Date: Thu, 2 Aug 2018 16:21:49 +0200 Subject: [PATCH 209/410] 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 Date: Thu, 2 Aug 2018 16:34:16 +0200 Subject: [PATCH 210/410] 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 Date: Thu, 2 Aug 2018 16:44:29 +0200 Subject: [PATCH 211/410] 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 Date: Thu, 2 Aug 2018 12:29:47 -0400 Subject: [PATCH 212/410] 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 Date: Thu, 2 Aug 2018 18:57:13 +0200 Subject: [PATCH 213/410] 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 Date: Thu, 26 Jul 2018 13:40:52 +0200 Subject: [PATCH 214/410] 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 Date: Thu, 26 Jul 2018 16:46:36 +0200 Subject: [PATCH 215/410] 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 Date: Sat, 28 Jul 2018 23:00:59 +0200 Subject: [PATCH 216/410] 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 Date: Sun, 29 Jul 2018 12:04:31 +0200 Subject: [PATCH 217/410] 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 Date: Mon, 30 Jul 2018 14:05:56 +0200 Subject: [PATCH 218/410] 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 Date: Mon, 30 Jul 2018 16:35:30 +0200 Subject: [PATCH 219/410] 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 Date: Mon, 30 Jul 2018 18:52:49 +0200 Subject: [PATCH 220/410] 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 Date: Mon, 30 Jul 2018 18:54:34 +0200 Subject: [PATCH 221/410] 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 Date: Mon, 30 Jul 2018 11:04:04 +0200 Subject: [PATCH 222/410] 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.
    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) -> DATEDIFF(second, '1970-01-01', column) AS time - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 -- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. +- $__timeGroup(column, '5m'[, fillvalue]) -> 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]) -> 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) -> UNIX_TIMESTAMP(column) as time_sec - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 -- $__timeGroup(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) +- $__timeGroup(column,'5m'[, fillvalue]) -> 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') -> 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 -> extract(epoch from column) as "time" - $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 -- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 +- $__timeGroup(column,'5m'[, fillvalue]) -> (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') -> (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 Date: Tue, 31 Jul 2018 06:36:45 +0200 Subject: [PATCH 223/410] 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.
    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 Date: Thu, 2 Aug 2018 17:42:28 +0200 Subject: [PATCH 224/410] 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 Date: Fri, 3 Aug 2018 10:20:13 +0200 Subject: [PATCH 225/410] 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 { 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 { } }; + 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 { ) : ( -
    - -
    - )} + + )} {!datasourceMissing ? (
    : null} + {supportsTable && showingTable ?
    : null} {supportsLogs && showingLogs ? : null} 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 { - 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 { }; render() { - const { request } = this.props; - const { edited, query } = this.state; + const { request, query, edited } = this.props; return (
    @@ -74,7 +62,9 @@ export default class QueryRows extends PureComponent { const { className = '', queries, ...handlers } = this.props; return (
    - {queries.map((q, index) => )} + {queries.map((q, index) => ( + + ))}
    ); } 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 { +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 ( +
    + ); + } + return ; +} + +export default class Table extends PureComponent { render() { - const { className = '', data } = this.props; + const { className = '', data, onClickCell } = this.props; const tableModel = data || EMPTY_TABLE; return (
    + + {value} + + {value}
    @@ -16,7 +46,13 @@ export default class Table extends PureComponent { {tableModel.columns.map(col => )} - {tableModel.rows.map((row, i) => {row.map((content, j) => )})} + {tableModel.rows.map((row, i) => ( + + {row.map((value, j) => ( + + ))} + + ))}
    {col.text}
    {content}
    ); 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 Date: Fri, 3 Aug 2018 11:57:03 +0200 Subject: [PATCH 226/410] 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 Date: Fri, 3 Aug 2018 12:00:20 +0200 Subject: [PATCH 227/410] 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 Date: Fri, 3 Aug 2018 12:34:13 +0200 Subject: [PATCH 228/410] 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 Date: Fri, 3 Aug 2018 13:09:05 +0200 Subject: [PATCH 229/410] 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 Date: Thu, 2 Aug 2018 13:39:05 +0200 Subject: [PATCH 230/410] 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 Date: Thu, 2 Aug 2018 13:52:10 +0200 Subject: [PATCH 231/410] 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 Date: Thu, 2 Aug 2018 14:35:49 +0200 Subject: [PATCH 232/410] 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 Date: Thu, 2 Aug 2018 14:46:02 +0200 Subject: [PATCH 233/410] 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 Date: Thu, 2 Aug 2018 15:06:41 +0200 Subject: [PATCH 234/410] 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 Date: Thu, 2 Aug 2018 15:55:03 +0200 Subject: [PATCH 235/410] 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 Date: Fri, 3 Aug 2018 11:00:27 +0200 Subject: [PATCH 236/410] 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 Date: Fri, 3 Aug 2018 11:37:33 +0200 Subject: [PATCH 237/410] 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 Date: Fri, 3 Aug 2018 12:19:41 +0200 Subject: [PATCH 238/410] 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 Date: Fri, 3 Aug 2018 12:20:46 +0200 Subject: [PATCH 239/410] 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?= Date: Sun, 5 Aug 2018 11:04:12 +0200 Subject: [PATCH 240/410] 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?= Date: Sun, 5 Aug 2018 11:18:49 +0200 Subject: [PATCH 241/410] 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?= Date: Sun, 5 Aug 2018 11:27:02 +0200 Subject: [PATCH 242/410] 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 Date: Mon, 6 Aug 2018 10:42:35 +0200 Subject: [PATCH 243/410] 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 = { + 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 Date: Mon, 6 Aug 2018 10:57:58 +0200 Subject: [PATCH 244/410] 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 = { + 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 Date: Wed, 1 Aug 2018 14:21:05 +0200 Subject: [PATCH 245/410] 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 = { + 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 Date: Wed, 1 Aug 2018 15:17:10 +0200 Subject: [PATCH 246/410] 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 Date: Wed, 1 Aug 2018 15:38:48 +0200 Subject: [PATCH 247/410] 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 = { @@ -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 Date: Wed, 1 Aug 2018 15:39:59 +0200 Subject: [PATCH 248/410] 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 Date: Mon, 6 Aug 2018 14:04:41 +0200 Subject: [PATCH 249/410] 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 Date: Mon, 6 Aug 2018 14:36:02 +0200 Subject: [PATCH 250/410] 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().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( - + ).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( - + ).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( - + ).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( - + ).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( + + ).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 { @@ -119,25 +121,23 @@ class PromQueryField extends React.Component { - 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 = 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 = 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 { 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 { + 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 Date: Mon, 6 Aug 2018 14:40:30 +0200 Subject: [PATCH 251/410] 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 Date: Thu, 2 Aug 2018 16:43:33 +0200 Subject: [PATCH 252/410] 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 { datasourceMissing: false, graphResult: null, initialDatasource: datasource, + history: [], latency: 0, loading: false, logsResult: null, @@ -138,6 +143,7 @@ export class Explore extends React.Component { 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 { 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 { } }; + 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 { 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 { 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 { 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 { datasourceLoading, datasourceMissing, graphResult, + history, latency, loading, logsResult, @@ -405,12 +440,12 @@ export class Explore extends React.Component {
    ) : ( -
    - -
    - )} +
    + )} {!datasourceMissing ? (
    { onExecuteQuery={this.handleSubmit} onRemoveQueryRow={this.handleRemoveQueryRow} /> - {queryError ?
    {queryError}
    : null} + {queryError && !loading ?
    {queryError}
    : null}
    {supportsGraph && showingGraph ? ( ) : null} {supportsTable && showingTable ? ( - +
    ) : null} - {supportsLogs && showingLogs ? : null} + {supportsLogs && showingLogs ? : null} ) : 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 { } render() { - const { data, height } = this.props; + const { data, height, loading } = this.props; + if (!loading && data && data.length === 0) { + return ( +
    +
    The queries returned no time series to graph.
    +
    + ); + } return (
    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 { 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
    The queries returned no data for a table.
    + ); + } return ( From 00f04f4ea0d0eab8ab1cc724b5431675e98d8d91 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 4 Aug 2018 11:47:04 +0200 Subject: [PATCH 261/410] 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 { } }; + 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 { ) : null} -
    - {supportsGraph ? ( - - ) : null} - {supportsTable ? ( - - ) : null} - {supportsLogs ? ( - - ) : null} -
    +
    + +
    + ) : null} + {supportsTable ? ( + + ) : null} + {supportsLogs ? ( + + ) : null} +
    +
    {supportsGraph && showingGraph ? ( { 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 { 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 Date: Sat, 4 Aug 2018 11:58:54 +0200 Subject: [PATCH 262/410] 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 { supportsTable, datasourceLoading: false, }, - () => datasourceError === null && this.handleSubmit() + () => datasourceError === null && this.onSubmit() ); } @@ -174,7 +174,7 @@ export class Explore extends React.Component { 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 { 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 { 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 { 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 { }); }; + 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 { 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 { ) : (
    -
    @@ -460,7 +460,7 @@ export class Explore extends React.Component {
    {row.map((value, j) => ( - + ))} ))} 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 Date: Tue, 7 Aug 2018 17:56:02 +0200 Subject: [PATCH 268/410] 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 Date: Tue, 7 Aug 2018 21:01:41 +0200 Subject: [PATCH 269/410] 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.
    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 Date: Tue, 7 Aug 2018 21:11:51 +0200 Subject: [PATCH 270/410] 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) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column, '5m'[, fillvalue]) -> 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]) -> 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) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 - $__timeGroup(column,'5m'[, fillvalue]) -> 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') -> 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) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column,'5m'[, fillvalue]) -> (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') -> (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 Date: Tue, 7 Aug 2018 22:32:02 +0200 Subject: [PATCH 271/410] 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": ''}* | 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 Date: Sun, 5 Aug 2018 13:54:06 -0700 Subject: [PATCH 272/410] 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 Date: Wed, 8 Aug 2018 09:13:44 +0200 Subject: [PATCH 273/410] 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 Date: Wed, 8 Aug 2018 09:23:36 +0200 Subject: [PATCH 274/410] 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 @@
    - + - +
    NameEmailMembers
    {{team.name}}{{team.email}}{{team.members}}
    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 Date: Wed, 8 Aug 2018 09:47:45 +0200 Subject: [PATCH 275/410] 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 Date: Wed, 8 Aug 2018 10:26:05 +0200 Subject: [PATCH 276/410] 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 Date: Wed, 8 Aug 2018 10:33:30 +0200 Subject: [PATCH 277/410] 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 Date: Wed, 8 Aug 2018 10:50:27 +0200 Subject: [PATCH 278/410] 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 @@ Name - Members - + {{team.name}} - {{team.members}} 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 Date: Wed, 8 Aug 2018 11:22:47 +0200 Subject: [PATCH 279/410] 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 @@ + + + + + +
    NameEmailMembers
    {{team.name}}{{team.email}}{{team.memberCount}}
    From 8dfe4a97efb0389f8c0ea77f823670a01e8361ae Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 8 Aug 2018 16:01:01 +0200 Subject: [PATCH 280/410] 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 Date: Wed, 8 Aug 2018 16:01:35 +0200 Subject: [PATCH 281/410] 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 Date: Wed, 8 Aug 2018 15:51:13 +0200 Subject: [PATCH 282/410] 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 Date: Wed, 8 Aug 2018 16:50:30 +0200 Subject: [PATCH 283/410] 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 { 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 { 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 { supportsLogs: null, supportsTable: null, tableResult: null, - ...props.initialState, + ...initialState, }; } @@ -191,6 +204,8 @@ export class Explore extends React.Component { 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 { 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 { }; 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 { }; 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 { 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 { 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 { 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 { } 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 { 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 { } 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 { loading, logsResult, queries, - queryError, + queryErrors, + queryHints, range, requestOptions, showingGraph, @@ -449,12 +497,12 @@ export class Explore extends React.Component {
    ) : ( -
    - -
    - )} +
    + )} {!datasourceMissing ? (
    + + This option determines whether TimescaleDB features will be used. + +
    +
    +
    +
    User Permission
    From c3aad100472063957ecf869115cde521c7d5ccf9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 9 Aug 2018 09:19:16 +0200 Subject: [PATCH 293/410] 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 @@
    - -
    - - - This option determines whether TimescaleDB features will be used. - -
    +
    From acd1acba2d426270ddb54a6e9b233562ec5f1ebd Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 9 Aug 2018 09:22:02 +0200 Subject: [PATCH 294/410] 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 Date: Thu, 9 Aug 2018 10:14:14 +0200 Subject: [PATCH 295/410] 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 Date: Thu, 9 Aug 2018 10:21:54 +0200 Subject: [PATCH 296/410] 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 Date: Thu, 9 Aug 2018 10:51:04 +0200 Subject: [PATCH 297/410] 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 Date: Thu, 9 Aug 2018 12:56:55 +0200 Subject: [PATCH 298/410] 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 { }; 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 { {member.login} {member.email} - - this.removeMember(member)} className="btn btn-danger btn-mini"> - - + + this.removeMember(member)} /> ); From 1bb3cf1c3116df8992e293a8f7a27d2c1d9d20e0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 9 Aug 2018 15:04:56 +0200 Subject: [PATCH 299/410] 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 Date: Thu, 9 Aug 2018 17:30:46 +0200 Subject: [PATCH 300/410] 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 @@
    - +
    From 9188f7423c6340c4898792b0fba594729869d19f Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Mon, 6 Aug 2018 09:06:29 +0200 Subject: [PATCH 301/410] 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 = ` +
    +
    +
    +
    +
    `; + + 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 Date: Fri, 10 Aug 2018 13:37:15 +0200 Subject: [PATCH 302/410] 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 { datasourceError: null, datasourceLoading: true, graphResult: null, + latency: 0, logsResult: null, queryErrors: [], queryHints: [], @@ -254,7 +255,10 @@ export class Explore extends React.Component { this.setState({ graphResult: null, logsResult: null, + latency: 0, queries: ensureQueries(), + queryErrors: [], + queryHints: [], tableResult: null, }); }; @@ -276,8 +280,10 @@ export class Explore extends React.Component { 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 Date: Fri, 10 Aug 2018 14:02:51 +0200 Subject: [PATCH 303/410] 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 Date: Fri, 10 Aug 2018 14:45:09 +0200 Subject: [PATCH 304/410] 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( + + ).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( 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 -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 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 Date: Fri, 10 Aug 2018 15:29:21 +0200 Subject: [PATCH 305/410] 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 Date: Fri, 10 Aug 2018 15:35:47 +0200 Subject: [PATCH 306/410] 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 Date: Fri, 10 Aug 2018 16:30:51 +0200 Subject: [PATCH 307/410] 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 = {}; + 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 = ` -
    -
    -
    -
    -
    `; + // let elemHtml = ` + //
    + //
    + //
    + //
    + //
    `; - 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 Date: Sat, 11 Aug 2018 23:42:31 -0700 Subject: [PATCH 308/410] 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 Date: Sat, 11 Aug 2018 23:44:15 -0700 Subject: [PATCH 309/410] 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 Date: Sun, 12 Aug 2018 10:51:58 +0200 Subject: [PATCH 310/410] 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 Date: Mon, 13 Aug 2018 07:53:41 +0200 Subject: [PATCH 311/410] Add support for $__range_s (#12883) Fixes #12882 Signed-off-by: Julien Pivotto --- 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([$__range]) != ) +Query: query_result(max_over_time([${__range_s}s]) != ) 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?= Date: Mon, 13 Aug 2018 07:54:49 +0200 Subject: [PATCH 312/410] 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 Date: Thu, 9 Aug 2018 16:29:16 +0200 Subject: [PATCH 313/410] 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 Date: Mon, 13 Aug 2018 09:20:17 +0200 Subject: [PATCH 314/410] 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 Date: Mon, 13 Aug 2018 10:57:32 +0200 Subject: [PATCH 315/410] 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 Date: Mon, 13 Aug 2018 11:14:24 +0200 Subject: [PATCH 316/410] 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 Date: Mon, 13 Aug 2018 11:16:49 +0200 Subject: [PATCH 317/410] 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 Date: Mon, 13 Aug 2018 11:34:16 +0200 Subject: [PATCH 318/410] 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 { 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 (
    @@ -81,7 +82,7 @@ export class TeamMembers extends React.Component { type="text" className="gf-form-input" placeholder="Search members" - value={''} + value={team.search} onChange={this.onSearchQueryChange} /> From bfe28ee061ea42b27057c582f0b436cf12c46e88 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 13 Aug 2018 12:08:14 +0200 Subject: [PATCH 319/410] 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') -> (extract(epoch from column)/300)::bigint*300 AS "time" +- $__unixEpochGroup(column,'5m') -> floor(column/300)*300 +- $__unixEpochGroupAlias(column,'5m') -> 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 Date: Mon, 13 Aug 2018 12:17:05 +0200 Subject: [PATCH 320/410] 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') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed) AS "time" +- $__unixEpochGroup(column,'5m') -> column DIV 300 * 300 +- $__unixEpochGroupAlias(column,'5m') -> 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 Date: Mon, 13 Aug 2018 12:23:42 +0200 Subject: [PATCH 321/410] 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]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300 AS [time] +- $__unixEpochGroup(column,'5m') -> FLOOR(column/300)*300 +- $__unixEpochGroupAlias(column,'5m') -> 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 Date: Mon, 13 Aug 2018 12:53:12 +0200 Subject: [PATCH 322/410] 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 Date: Mon, 13 Aug 2018 13:55:47 +0200 Subject: [PATCH 323/410] 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 = {}; + 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 Date: Mon, 13 Aug 2018 14:24:15 +0200 Subject: [PATCH 324/410] 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 Date: Mon, 13 Aug 2018 15:07:29 +0200 Subject: [PATCH 325/410] 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 @@ -

    Teams

    +

    Teams

    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 Date: Mon, 13 Aug 2018 15:18:33 +0200 Subject: [PATCH 326/410] 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?= Date: Mon, 13 Aug 2018 15:38:28 +0200 Subject: [PATCH 327/410] 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 Date: Mon, 13 Aug 2018 15:38:37 +0200 Subject: [PATCH 328/410] 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?= Date: Mon, 13 Aug 2018 15:40:37 +0200 Subject: [PATCH 329/410] 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 Date: Mon, 13 Aug 2018 15:40:52 +0200 Subject: [PATCH 330/410] 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 @@ - + @@ -107,12 +107,12 @@ [[end]] - + \ No newline at end of file From 39669e5002207fd0b486eeb49a0fa417b51a1e09 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 13 Aug 2018 15:41:15 +0200 Subject: [PATCH 331/410] 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 Date: Mon, 13 Aug 2018 15:51:19 +0200 Subject: [PATCH 332/410] 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 Date: Mon, 13 Aug 2018 15:51:58 +0200 Subject: [PATCH 333/410] 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
    ; + } +} From c521f51780b12937cdc1c9c844f92d9515190320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 13 Aug 2018 15:56:11 +0200 Subject: [PATCH 334/410] 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 Date: Mon, 13 Aug 2018 16:08:01 +0200 Subject: [PATCH 335/410] 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; + panel: any; + plot: any; + sortedSeries: Array; + data: Array; + 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) { + $("
    ") + .text(this.panel.yaxes[0].label) + .appendTo(this.elem); + } + + // add right axis labels + if (this.panel.yaxes[1].label && this.panel.yaxes[1].show) { + $("
    ") + .text(this.panel.yaxes[1].label) + .appendTo(this.elem); + } + + if (this.ctrl.dataWarning) { + $(`
    ${this.ctrl.dataWarning.title}
    `).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) { - $("
    ") - .text(panel.yaxes[0].label) - .appendTo(elem); - } - - // add right axis labels - if (panel.yaxes[1].label && panel.yaxes[1].show) { - $("
    ") - .text(panel.yaxes[1].label) - .appendTo(elem); - } - - if (ctrl.dataWarning) { - $(`
    ${ctrl.dataWarning.title}
    `).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 = {}; +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 = { + ...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("
    "); - $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 Date: Wed, 1 Aug 2018 17:11:29 +0200 Subject: [PATCH 336/410] 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 = ''; + $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 = ''; - // Older browser does not the internationalization API - if (!(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 = (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 (!(window).Intl) { + return utcOffset; + } - return '&tz=' + encodeURIComponent(options.timeZone); - }; + const dateFormat = (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 = { + 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 Date: Fri, 3 Aug 2018 14:37:31 +0200 Subject: [PATCH 337/410] 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: () => {}, }, }; + + (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 Date: Fri, 3 Aug 2018 14:40:44 +0200 Subject: [PATCH 338/410] 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 Date: Fri, 3 Aug 2018 14:44:40 +0200 Subject: [PATCH 339/410] 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?= Date: Mon, 13 Aug 2018 18:09:01 +0200 Subject: [PATCH 340/410] 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 Date: Tue, 14 Aug 2018 08:34:20 +0200 Subject: [PATCH 341/410] 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?= Date: Tue, 14 Aug 2018 08:49:56 +0200 Subject: [PATCH 342/410] 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 Date: Tue, 14 Aug 2018 08:50:22 +0200 Subject: [PATCH 343/410] 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 @@
    - @@ -53,7 +53,7 @@ export class TeamGroupSync extends React.Component { 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 { 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 { 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 { 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 (
    +
    {{col.title}} 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?= Date: Tue, 14 Aug 2018 08:52:30 +0200 Subject: [PATCH 344/410] 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?= Date: Tue, 14 Aug 2018 08:52:51 +0200 Subject: [PATCH 345/410] 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?= Date: Tue, 14 Aug 2018 08:55:27 +0200 Subject: [PATCH 346/410] 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 Date: Tue, 14 Aug 2018 09:15:14 +0200 Subject: [PATCH 347/410] 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
    ; - } -} 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 = {}; - 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 = ` - //
    - //
    - //
    - //
    - //
    `; - - // 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 = ` -
    -
    -
    -
    -
    `; - - 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 Date: Tue, 14 Aug 2018 09:15:24 +0200 Subject: [PATCH 348/410] 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?= Date: Tue, 14 Aug 2018 09:18:04 +0200 Subject: [PATCH 349/410] 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 Date: Tue, 14 Aug 2018 09:19:37 +0200 Subject: [PATCH 350/410] 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 Date: Tue, 14 Aug 2018 09:28:08 +0200 Subject: [PATCH 351/410] 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 Date: Tue, 14 Aug 2018 09:44:58 +0200 Subject: [PATCH 352/410] 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 Date: Tue, 14 Aug 2018 10:22:57 +0200 Subject: [PATCH 353/410] 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 Date: Tue, 14 Aug 2018 10:26:08 +0200 Subject: [PATCH 354/410] 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 Date: Tue, 14 Aug 2018 10:35:34 +0200 Subject: [PATCH 355/410] 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 Date: Tue, 14 Aug 2018 10:52:41 +0200 Subject: [PATCH 356/410] 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": [ "/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 Date: Tue, 14 Aug 2018 11:23:55 +0200 Subject: [PATCH 357/410] 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 Date: Tue, 14 Aug 2018 11:24:08 +0200 Subject: [PATCH 358/410] 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 Date: Tue, 14 Aug 2018 12:16:46 +0200 Subject: [PATCH 359/410] 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?= Date: Tue, 14 Aug 2018 12:25:19 +0200 Subject: [PATCH 360/410] 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 Date: Tue, 14 Aug 2018 12:40:07 +0200 Subject: [PATCH 361/410] 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 Date: Tue, 14 Aug 2018 13:42:18 +0200 Subject: [PATCH 362/410] 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 Date: Tue, 14 Aug 2018 10:45:32 +0200 Subject: [PATCH 363/410] 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?= Date: Thu, 15 Feb 2018 19:13:47 +0100 Subject: [PATCH 364/410] 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 @@ Sign in with GitHub + - -
    {group.groupId}
    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 { 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 { - render() { - const { - title, - buttonIcon, - buttonLink, - buttonTitle, - proTip, - proTipLink, - proTipLinkTitle, - proTipTarget - } = this.props.model; - return ( -
    -
    {title}
    - {buttonTitle} -
    - ProTip: {proTip} - {proTipLinkTitle} -
    -
    - ); - } +class EmptyListCTA extends Component { + render() { + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget, + } = this.props.model; + return ( +
    +
    {title}
    + + + {buttonTitle} + +
    + ProTip: {proTip} + + {proTipLinkTitle} + +
    +
    + ); + } } 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 { +export default class PageHeader extends React.Component { 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 { - +export class PasswordStrength extends React.Component { 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 { ); } } - - 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 { +export default class DisabledPermissionListItem extends Component { 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 { +class Permissions extends Component { 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 { +class PermissionsList extends Component { 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 { +class DescriptionOption extends Component { 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 { +class UserPickerOption extends Component { 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 { +export class TagBadge extends React.Component { 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 { +export class TagFilter extends React.Component { 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 { +export class TagOption extends React.Component { 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 { +export class TagValue extends React.Component { 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 { +class Popover extends React.Component { 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 { +class Tooltip extends React.Component { 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 { +export class ColorPalette extends React.Component { paletteColors: string[]; constructor(props) { @@ -29,7 +29,8 @@ export class ColorPalette extends React.Component { key={paletteColor} className={'pointer fa ' + cssClass} style={{ color: paletteColor }} - onClick={this.onColorSelect(paletteColor)}> + onClick={this.onColorSelect(paletteColor)} + >   ); @@ -41,4 +42,3 @@ export class ColorPalette extends React.Component { ); } } - 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 { +export class ColorPicker extends React.Component { 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 { +export class ColorPickerPopover extends React.Component { pickerNavElem: any; constructor(props) { @@ -19,7 +19,7 @@ export class ColorPickerPopover extends React.Component { 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 { 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 { 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 { 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 {
    -
    - {currentTab} -
    +
    {currentTab}
    - - +
    ); 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 { +export class SeriesColorPicker extends React.Component { 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 { +export class SpectrumPicker extends React.Component { elem: any; isMoving: boolean; @@ -29,14 +29,17 @@ export class SpectrumPicker extends React.Component { } 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 { } render() { - return ( -
    - ); + return
    ; } } - 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" From 9b978b7203afdde901fa0d4324719b2aa64db271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 26 Aug 2018 17:14:40 +0200 Subject: [PATCH 407/410] tslint: autofix of let -> const (#13033) --- .../AlertRuleList/AlertRuleList.test.tsx | 2 +- .../AlertRuleList/AlertRuleList.tsx | 4 +- public/app/containers/Explore/Table.tsx | 2 +- .../containers/Explore/utils/prometheus.ts | 2 +- public/app/containers/Teams/TeamList.tsx | 2 +- .../Permissions/AddPermissions.test.tsx | 2 +- .../core/components/TagFilter/TagFilter.tsx | 2 +- .../components/code_editor/code_editor.ts | 30 ++-- .../components/colorpicker/ColorPicker.tsx | 4 +- .../colorpicker/ColorPickerPopover.tsx | 14 +- .../components/colorpicker/SpectrumPicker.tsx | 2 +- .../components/form_dropdown/form_dropdown.ts | 2 +- public/app/core/components/grafana_app.ts | 2 +- public/app/core/components/info_popover.ts | 14 +- .../manage_dashboards/manage_dashboards.ts | 14 +- public/app/core/components/scroll/scroll.ts | 6 +- .../core/components/search/SearchResult.tsx | 2 +- .../app/core/components/sidemenu/sidemenu.ts | 4 +- public/app/core/controllers/login_ctrl.ts | 4 +- .../app/core/directives/dropdown_typeahead.ts | 32 ++--- public/app/core/directives/metric_segment.ts | 28 ++-- public/app/core/directives/misc.ts | 2 +- .../core/directives/value_select_dropdown.ts | 18 +-- public/app/core/nav_model_srv.ts | 6 +- public/app/core/services/backend_srv.ts | 10 +- public/app/core/services/bridge_srv.ts | 4 +- .../core/services/dynamic_directive_srv.ts | 2 +- public/app/core/services/keybindingSrv.ts | 2 +- public/app/core/services/ng_react.ts | 2 +- public/app/core/services/search_srv.ts | 14 +- public/app/core/services/segment_srv.ts | 4 +- public/app/core/specs/backend_srv.test.ts | 4 +- public/app/core/specs/file_export.test.ts | 6 +- public/app/core/specs/search.test.ts | 2 +- public/app/core/specs/search_results.test.ts | 8 +- public/app/core/specs/ticks.test.ts | 2 +- public/app/core/specs/time_series.test.ts | 10 +- .../core/specs/value_select_dropdown.test.ts | 2 +- public/app/core/time_series2.ts | 8 +- public/app/core/utils/colors.ts | 4 +- public/app/core/utils/dag.test.ts | 20 +-- public/app/core/utils/dag.ts | 18 +-- public/app/core/utils/file_export.ts | 16 +-- public/app/core/utils/outline.ts | 2 +- public/app/core/utils/rangeutil.ts | 10 +- public/app/core/utils/sort_by_keys.ts | 2 +- public/app/core/utils/tags.ts | 6 +- public/app/core/utils/ticks.ts | 22 +-- public/app/core/utils/url.ts | 10 +- public/app/core/utils/version.ts | 6 +- .../app/features/alerting/alert_tab_ctrl.ts | 4 +- .../alerting/notification_edit_ctrl.ts | 2 +- .../app/features/alerting/threshold_mapper.ts | 14 +- .../features/annotations/annotations_srv.ts | 2 +- .../app/features/annotations/event_editor.ts | 4 +- .../app/features/annotations/event_manager.ts | 12 +- .../features/annotations/events_processing.ts | 12 +- .../annotations/specs/annotations_srv.test.ts | 4 +- .../specs/annotations_srv_specs.test.ts | 8 +- .../app/features/dashboard/ad_hoc_filters.ts | 2 +- .../app/features/dashboard/change_tracker.ts | 8 +- .../dashboard/dashboard_import_ctrl.ts | 4 +- .../features/dashboard/dashboard_migration.ts | 14 +- .../app/features/dashboard/dashboard_model.ts | 88 ++++++------ .../dashboard/dashgrid/AddPanelPanel.tsx | 20 +-- .../dashboard/dashgrid/DashboardGrid.tsx | 8 +- .../app/features/dashboard/dashnav/dashnav.ts | 4 +- .../features/dashboard/export/export_modal.ts | 2 +- .../app/features/dashboard/export/exporter.ts | 14 +- .../app/features/dashboard/history/history.ts | 4 +- .../features/dashboard/settings/settings.ts | 2 +- .../app/features/dashboard/shareModalCtrl.ts | 2 +- .../specs/dashboard_migration.test.ts | 84 +++++------ .../dashboard/specs/dashboard_model.test.ts | 32 ++--- .../dashboard/specs/history_ctrl.test.ts | 4 +- .../dashboard/specs/history_srv.test.ts | 6 +- .../features/dashboard/specs/repeat.test.ts | 2 +- .../dashboard/specs/viewstate_srv.test.ts | 6 +- .../app/features/dashboard/validation_srv.ts | 4 +- .../app/features/dashboard/view_state_srv.ts | 4 +- public/app/features/org/org_users_ctrl.ts | 2 +- public/app/features/panel/metrics_tab.ts | 2 +- public/app/features/panel/panel_ctrl.ts | 16 +-- public/app/features/panel/panel_directive.ts | 4 +- public/app/features/panel/panel_header.ts | 12 +- public/app/features/panel/solo_panel_ctrl.ts | 2 +- .../panellinks/specs/link_srv.test.ts | 2 +- .../app/features/playlist/playlist_routes.ts | 2 +- .../playlist/specs/playlist_edit_ctrl.test.ts | 2 +- public/app/features/plugins/ds_list_ctrl.ts | 2 +- .../app/features/plugins/plugin_component.ts | 8 +- .../app/features/plugins/plugin_edit_ctrl.ts | 6 +- .../app/features/plugins/plugin_list_ctrl.ts | 2 +- public/app/features/plugins/plugin_loader.ts | 4 +- .../app/features/plugins/plugin_page_ctrl.ts | 2 +- .../plugins/specs/datasource_srv.test.ts | 4 +- .../templating/specs/editor_ctrl.test.ts | 2 +- .../specs/variable_srv_init.test.ts | 6 +- .../app/features/templating/variable_srv.ts | 12 +- .../datasource/cloudwatch/datasource.ts | 4 +- .../cloudwatch/specs/datasource.test.ts | 26 ++-- .../datasource/elasticsearch/datasource.ts | 8 +- .../elasticsearch/elastic_response.ts | 14 +- .../elasticsearch/specs/datasource.test.ts | 10 +- .../plugins/datasource/grafana/datasource.ts | 2 +- .../plugins/datasource/graphite/datasource.ts | 32 ++--- .../datasource/graphite/graphite_query.ts | 18 +-- .../plugins/datasource/graphite/query_ctrl.ts | 30 ++-- .../graphite/specs/datasource.test.ts | 30 ++-- .../graphite/specs/graphite_query.test.ts | 2 +- .../graphite/specs/query_ctrl.test.ts | 2 +- .../plugins/datasource/influxdb/datasource.ts | 12 +- .../datasource/influxdb/influx_query.ts | 4 +- .../plugins/datasource/influxdb/query_ctrl.ts | 6 +- .../influxdb/specs/datasource.test.ts | 6 +- .../influxdb/specs/query_ctrl.test.ts | 2 +- .../plugins/datasource/mssql/query_ctrl.ts | 4 +- .../datasource/mssql/response_parser.ts | 8 +- .../plugins/datasource/mysql/query_ctrl.ts | 4 +- .../datasource/mysql/response_parser.ts | 8 +- .../datasource/mysql/specs/datasource.test.ts | 8 +- .../opentsdb/specs/datasource.test.ts | 4 +- .../plugins/datasource/postgres/query_ctrl.ts | 4 +- .../datasource/postgres/response_parser.ts | 10 +- .../postgres/specs/datasource.test.ts | 8 +- .../datasource/prometheus/completer.ts | 20 +-- .../datasource/prometheus/datasource.ts | 18 +-- .../prometheus/result_transformer.ts | 16 +-- .../prometheus/specs/completer.test.ts | 10 +- .../prometheus/specs/datasource.test.ts | 84 +++++------ .../specs/metric_find_query.test.ts | 4 +- .../specs/result_transformer.test.ts | 10 +- .../plugins/datasource/testdata/datasource.ts | 2 +- public/app/plugins/panel/alertlist/module.ts | 4 +- .../app/plugins/panel/graph/data_processor.ts | 12 +- public/app/plugins/panel/graph/graph.ts | 24 ++-- .../app/plugins/panel/graph/graph_tooltip.ts | 32 ++--- public/app/plugins/panel/graph/histogram.ts | 20 +-- .../plugins/panel/graph/jquery.flot.events.ts | 90 ++++++------ public/app/plugins/panel/graph/legend.ts | 14 +- public/app/plugins/panel/graph/module.ts | 4 +- .../plugins/panel/graph/specs/graph.test.ts | 4 +- .../panel/graph/specs/graph_ctrl.test.ts | 6 +- .../panel/graph/specs/histogram.test.ts | 16 +-- .../graph/specs/series_override_ctrl.test.ts | 2 +- .../app/plugins/panel/heatmap/color_legend.ts | 118 +++++++-------- .../app/plugins/panel/heatmap/color_scale.ts | 8 +- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 50 +++---- .../panel/heatmap/heatmap_data_converter.ts | 78 +++++----- .../plugins/panel/heatmap/heatmap_tooltip.ts | 52 +++---- public/app/plugins/panel/heatmap/rendering.ts | 134 +++++++++--------- .../panel/heatmap/specs/heatmap_ctrl.test.ts | 6 +- .../specs/heatmap_data_converter.test.ts | 40 +++--- public/app/plugins/panel/pluginlist/module.ts | 2 +- public/app/plugins/panel/singlestat/module.ts | 14 +- .../panel/singlestat/specs/singlestat.test.ts | 8 +- public/app/plugins/panel/table/module.ts | 2 +- public/app/plugins/panel/table/renderer.ts | 26 ++-- .../app/plugins/panel/table/transformers.ts | 2 +- .../stores/AlertListStore/AlertListStore.ts | 4 +- public/app/stores/NavStore/NavStore.ts | 14 +- .../PermissionsStore/PermissionsStore.ts | 6 +- public/app/stores/TeamsStore/TeamsStore.ts | 14 +- public/app/stores/ViewStore/ViewStore.ts | 4 +- public/test/core/utils/version_test.ts | 42 +++--- public/test/index.ts | 6 +- public/test/mocks/common.ts | 6 +- 167 files changed, 1077 insertions(+), 1081 deletions(-) diff --git a/public/app/containers/AlertRuleList/AlertRuleList.test.tsx b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx index eac18a6c69d..f88ff4522d4 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.test.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx @@ -46,7 +46,7 @@ describe('AlertRuleList', () => { it('should render 1 rule', () => { page.update(); - let ruleNode = page.find('.alert-rule-item'); + const ruleNode = page.find('.alert-rule-item'); expect(toJson(ruleNode)).toMatchSnapshot(); }); diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx index 3c2da77c2a7..668136dee6f 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx @@ -132,13 +132,13 @@ export class AlertRuleItem extends React.Component { render() { const { rule } = this.props; - let stateClass = classNames({ + const stateClass = classNames({ fa: true, 'fa-play': rule.isPaused, 'fa-pause': !rule.isPaused, }); - let ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; + const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; return (
  • diff --git a/public/app/containers/Explore/Table.tsx b/public/app/containers/Explore/Table.tsx index e5adde2d008..cbb3ab11f4e 100644 --- a/public/app/containers/Explore/Table.tsx +++ b/public/app/containers/Explore/Table.tsx @@ -40,7 +40,7 @@ function Cell(props: SFCCellProps) { export default class Table extends PureComponent { render() { const { className = '', data, loading, onClickCell } = this.props; - let tableModel = data || EMPTY_TABLE; + const tableModel = data || EMPTY_TABLE; if (!loading && data && data.rows.length === 0) { return ( diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts index f5ccb848f2f..19129976282 100644 --- a/public/app/containers/Explore/utils/prometheus.ts +++ b/public/app/containers/Explore/utils/prometheus.ts @@ -65,7 +65,7 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any // Extract clean labels to form clean selector, incomplete labels are dropped const selector = query.slice(prefixOpen, suffixClose); - let labels = {}; + const labels = {}; selector.replace(labelRegexp, match => { const delimiterIndex = match.indexOf('='); const key = match.slice(0, delimiterIndex); diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 2d037eed642..d0feee75184 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -36,7 +36,7 @@ export class TeamList extends React.Component { }; renderTeamMember(team: Team): JSX.Element { - let teamUrl = `org/teams/edit/${team.id}`; + const teamUrl = `org/teams/edit/${team.id}`; return ( diff --git a/public/app/core/components/Permissions/AddPermissions.test.tsx b/public/app/core/components/Permissions/AddPermissions.test.tsx index 513a22ddea4..c6d1ab381b8 100644 --- a/public/app/core/components/Permissions/AddPermissions.test.tsx +++ b/public/app/core/components/Permissions/AddPermissions.test.tsx @@ -22,7 +22,7 @@ describe('AddPermissions', () => { let wrapper; let store; let instance; - let backendSrv: any = getBackendSrv(); + const backendSrv: any = getBackendSrv(); beforeAll(() => { store = RootStore.create({}, { backendSrv: backendSrv }); diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 84f3e1819cd..a879f544da0 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -43,7 +43,7 @@ export class TagFilter extends React.Component { } render() { - let selectOptions = { + const selectOptions = { loadOptions: this.searchTags, onChange: this.onChange, value: this.props.tags, diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 886ae2a6407..66aec778d73 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -53,23 +53,23 @@ const DEFAULT_TAB_SIZE = 2; const DEFAULT_BEHAVIOURS = true; const DEFAULT_SNIPPETS = true; -let editorTemplate = `
    `; +const editorTemplate = `
    `; function link(scope, elem, attrs) { // Options - let langMode = attrs.mode || DEFAULT_MODE; - let maxLines = attrs.maxLines || DEFAULT_MAX_LINES; - let showGutter = attrs.showGutter !== undefined; - let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; - let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; - let snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; + const langMode = attrs.mode || DEFAULT_MODE; + const maxLines = attrs.maxLines || DEFAULT_MAX_LINES; + const showGutter = attrs.showGutter !== undefined; + const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; + const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; + const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; // Initialize editor - let aceElem = elem.get(0); - let codeEditor = ace.edit(aceElem); - let editorSession = codeEditor.getSession(); + const aceElem = elem.get(0); + const codeEditor = ace.edit(aceElem); + const editorSession = codeEditor.getSession(); - let editorOptions = { + const editorOptions = { maxLines: maxLines, showGutter: showGutter, tabSize: tabSize, @@ -93,7 +93,7 @@ function link(scope, elem, attrs) { // Add classes elem.addClass('gf-code-editor'); - let textarea = elem.find('textarea'); + const textarea = elem.find('textarea'); textarea.addClass('gf-form-input'); if (scope.codeEditorFocus) { @@ -110,14 +110,14 @@ function link(scope, elem, attrs) { // Event handlers editorSession.on('change', e => { scope.$apply(() => { - let newValue = codeEditor.getValue(); + const newValue = codeEditor.getValue(); scope.content = newValue; }); }); // Sync with outer scope - update editor content if model has been changed from outside of directive. scope.$watch('content', (newValue, oldValue) => { - let editorValue = codeEditor.getValue(); + const editorValue = codeEditor.getValue(); if (newValue !== editorValue && newValue !== oldValue) { scope.$$postDigest(function() { setEditorContent(newValue); @@ -157,7 +157,7 @@ function link(scope, elem, attrs) { anyEditor.completers.push(scope.getCompleter()); } - let aceModeName = `ace/mode/${lang}`; + const aceModeName = `ace/mode/${lang}`; editorSession.setMode(aceModeName); } diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx index c492d3829ca..6e5083b6d6b 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/public/app/core/components/colorpicker/ColorPicker.tsx @@ -29,10 +29,10 @@ export class ColorPicker extends React.Component { openColorPicker() { const dropContent = ; - let dropContentElem = document.createElement('div'); + const dropContentElem = document.createElement('div'); ReactDOM.render(dropContent, dropContentElem); - let drop = new Drop({ + const drop = new Drop({ target: this.pickerElem[0], content: dropContentElem, position: 'top center', diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index ac7dd6a2738..c42bcfa1d06 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -28,7 +28,7 @@ export class ColorPickerPopover extends React.Component { } setColor(color) { - let newColor = tinycolor(color); + const newColor = tinycolor(color); if (newColor.isValid()) { this.setState({ color: newColor.toString(), @@ -43,20 +43,20 @@ export class ColorPickerPopover extends React.Component { } spectrumColorSelected(color) { - let rgbColor = color.toRgbString(); + const rgbColor = color.toRgbString(); this.setColor(rgbColor); } onColorStringChange(e) { - let colorString = e.target.value; + const colorString = e.target.value; this.setState({ colorString: colorString, }); - let newColor = tinycolor(colorString); + const newColor = tinycolor(colorString); if (newColor.isValid()) { // Update only color state - let newColorString = newColor.toString(); + const newColorString = newColor.toString(); this.setState({ color: newColorString, }); @@ -65,7 +65,7 @@ export class ColorPickerPopover extends React.Component { } onColorStringBlur(e) { - let colorString = e.target.value; + const colorString = e.target.value; this.setColor(colorString); } @@ -73,7 +73,7 @@ export class ColorPickerPopover extends React.Component { this.pickerNavElem.find('li:first').addClass('active'); this.pickerNavElem.on('show', e => { // use href attr (#name => name) - let tab = e.target.hash.slice(1); + const tab = e.target.hash.slice(1); this.setState({ tab: tab, }); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx index e8a30e8c460..15a76068e9b 100644 --- a/public/app/core/components/colorpicker/SpectrumPicker.tsx +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -29,7 +29,7 @@ export class SpectrumPicker extends React.Component { } componentDidMount() { - let spectrumOptions = _.assignIn( + const spectrumOptions = _.assignIn( { flat: true, showAlpha: true, diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 7ac55e54cf1..007c7c3acb1 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -132,7 +132,7 @@ export class FormDropdownCtrl { this.optionCache = options; // extract texts - let optionTexts = _.map(options, op => { + const optionTexts = _.map(options, op => { return _.escape(op.text); }); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index bd6b6975006..1f55bc332ac 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -140,7 +140,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } // close all drops - for (let drop of Drop.drops) { + for (const drop of Drop.drops) { drop.destroy(); } }); diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index 59332a6f716..ae4feeec701 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -8,10 +8,10 @@ export function infoPopover() { template: '', transclude: true, link: function(scope, elem, attrs, ctrl, transclude) { - let offset = attrs.offset || '0 -10px'; - let position = attrs.position || 'right middle'; + const offset = attrs.offset || '0 -10px'; + const position = attrs.position || 'right middle'; let classes = 'drop-help drop-hide-out-of-bounds'; - let openOn = 'hover'; + const openOn = 'hover'; elem.addClass('gf-form-help-icon'); @@ -24,14 +24,14 @@ export function infoPopover() { } transclude(function(clone, newScope) { - let content = document.createElement('div'); + const content = document.createElement('div'); content.className = 'markdown-html'; _.each(clone, node => { content.appendChild(node); }); - let dropOptions = { + const dropOptions = { target: elem[0], content: content, position: position, @@ -52,9 +52,9 @@ export function infoPopover() { // Create drop in next digest after directive content is rendered. scope.$applyAsync(() => { - let drop = new Drop(dropOptions); + const drop = new Drop(dropOptions); - let unbind = scope.$on('$destroy', function() { + const unbind = scope.$on('$destroy', function() { drop.destroy(); unbind(); }); diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 86cd3066c48..59a34d08c12 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -103,10 +103,10 @@ export class ManageDashboardsCtrl { this.sections = result; - for (let section of this.sections) { + for (const section of this.sections) { section.checked = false; - for (let dashboard of section.items) { + for (const dashboard of section.items) { dashboard.checked = false; } } @@ -119,7 +119,7 @@ export class ManageDashboardsCtrl { selectionChanged() { let selectedDashboards = 0; - for (let section of this.sections) { + for (const section of this.sections) { selectedDashboards += _.filter(section.items, { checked: true }).length; } @@ -129,7 +129,7 @@ export class ManageDashboardsCtrl { } getFoldersAndDashboardsToDelete() { - let selectedDashboards = { + const selectedDashboards = { folders: [], dashboards: [], }; @@ -148,7 +148,7 @@ export class ManageDashboardsCtrl { getFolderIds(sections) { const ids = []; - for (let s of sections) { + for (const s of sections) { if (s.checked) { ids.push(s.id); } @@ -191,7 +191,7 @@ export class ManageDashboardsCtrl { } getDashboardsToMove() { - let selectedDashboards = []; + const selectedDashboards = []; for (const section of this.sections) { const selected = _.filter(section.items, { checked: true }); @@ -264,7 +264,7 @@ export class ManageDashboardsCtrl { } onSelectAllChanged() { - for (let section of this.sections) { + for (const section of this.sections) { if (!section.hideHeader) { section.checked = this.selectAllChecked; } diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index 3f9865e6dce..5cdbdb62ee3 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -17,7 +17,7 @@ export function geminiScrollbar() { restrict: 'A', link: function(scope, elem, attrs) { let scrollRoot = elem.parent(); - let scroller = elem; + const scroller = elem; if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') { scrollRoot = scroller; @@ -27,7 +27,7 @@ export function geminiScrollbar() { $(scrollBarHTML).appendTo(scrollRoot); elem.addClass(scrollerClass); - let scrollParams = { + const scrollParams = { root: scrollRoot[0], scroller: scroller[0], bar: '.baron__bar', @@ -37,7 +37,7 @@ export function geminiScrollbar() { direction: 'v', }; - let scrollbar = baron(scrollParams); + const scrollbar = baron(scrollParams); let lastPos = 0; diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx index 5ab4bba8edb..3141d29ac7f 100644 --- a/public/app/core/components/search/SearchResult.tsx +++ b/public/app/core/components/search/SearchResult.tsx @@ -54,7 +54,7 @@ export class SearchResultSection extends React.Component { }; render() { - let collapseClassNames = classNames({ + const collapseClassNames = classNames({ fa: true, 'fa-plus': !this.props.section.expanded, 'fa-minus': this.props.section.expanded, diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index fb9d9be7f70..5649963c3dc 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -17,13 +17,13 @@ export class SideMenuCtrl { this.isSignedIn = contextSrv.isSignedIn; this.user = contextSrv.user; - let navTree = _.cloneDeep(config.bootData.navTree); + const navTree = _.cloneDeep(config.bootData.navTree); this.mainLinks = _.filter(navTree, item => !item.hideFromMenu); this.bottomNav = _.filter(navTree, item => item.hideFromMenu); this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path()); if (contextSrv.user.orgCount > 1) { - let profileNode = _.find(this.bottomNav, { id: 'profile' }); + const profileNode = _.find(this.bottomNav, { id: 'profile' }); if (profileNode) { profileNode.showOrgSwitcher = true; } diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts index 0a66f83d08a..6662686b238 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -45,8 +45,8 @@ export class LoginCtrl { }; $scope.changeView = function() { - let loginView = document.querySelector('#login-view'); - let changePasswordView = document.querySelector('#change-password-view'); + const loginView = document.querySelector('#login-view'); + const changePasswordView = document.querySelector('#change-password-view'); loginView.className += ' add'; setTimeout(() => { diff --git a/public/app/core/directives/dropdown_typeahead.ts b/public/app/core/directives/dropdown_typeahead.ts index c9e44c5e786..af8c4ddc3bb 100644 --- a/public/app/core/directives/dropdown_typeahead.ts +++ b/public/app/core/directives/dropdown_typeahead.ts @@ -4,12 +4,12 @@ import coreModule from '../core_module'; /** @ngInject */ export function dropdownTypeahead($compile) { - let inputTemplate = + const inputTemplate = ''; - let buttonTemplate = + const buttonTemplate = ''; @@ -21,8 +21,8 @@ export function dropdownTypeahead($compile) { model: '=ngModel', }, link: function($scope, elem, attrs) { - let $input = $(inputTemplate); - let $button = $(buttonTemplate); + const $input = $(inputTemplate); + const $button = $(buttonTemplate); $input.appendTo(elem); $button.appendTo(elem); @@ -42,7 +42,7 @@ export function dropdownTypeahead($compile) { }); } - let typeaheadValues = _.reduce( + const typeaheadValues = _.reduce( $scope.menuItems, function(memo, value, index) { if (!value.submenu) { @@ -60,8 +60,8 @@ export function dropdownTypeahead($compile) { ); $scope.menuItemSelected = function(index, subIndex) { - let menuItem = $scope.menuItems[index]; - let payload: any = { $item: menuItem }; + const menuItem = $scope.menuItems[index]; + const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { payload.$subItem = menuItem.submenu[subIndex]; } @@ -74,7 +74,7 @@ export function dropdownTypeahead($compile) { minLength: 1, items: 10, updater: function(value) { - let result: any = {}; + const result: any = {}; _.each($scope.menuItems, function(menuItem) { _.each(menuItem.submenu, function(submenuItem) { if (value === menuItem.text + ' ' + submenuItem.text) { @@ -124,10 +124,10 @@ export function dropdownTypeahead($compile) { /** @ngInject */ export function dropdownTypeahead2($compile) { - let inputTemplate = + const inputTemplate = ''; - let buttonTemplate = + const buttonTemplate = ''; @@ -139,8 +139,8 @@ export function dropdownTypeahead2($compile) { model: '=ngModel', }, link: function($scope, elem, attrs) { - let $input = $(inputTemplate); - let $button = $(buttonTemplate); + const $input = $(inputTemplate); + const $button = $(buttonTemplate); $input.appendTo(elem); $button.appendTo(elem); @@ -160,7 +160,7 @@ export function dropdownTypeahead2($compile) { }); } - let typeaheadValues = _.reduce( + const typeaheadValues = _.reduce( $scope.menuItems, function(memo, value, index) { if (!value.submenu) { @@ -178,8 +178,8 @@ export function dropdownTypeahead2($compile) { ); $scope.menuItemSelected = function(index, subIndex) { - let menuItem = $scope.menuItems[index]; - let payload: any = { $item: menuItem }; + const menuItem = $scope.menuItems[index]; + const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { payload.$subItem = menuItem.submenu[subIndex]; } @@ -192,7 +192,7 @@ export function dropdownTypeahead2($compile) { minLength: 1, items: 10, updater: function(value) { - let result: any = {}; + const result: any = {}; _.each($scope.menuItems, function(menuItem) { _.each(menuItem.submenu, function(submenuItem) { if (value === menuItem.text + ' ' + submenuItem.text) { diff --git a/public/app/core/directives/metric_segment.ts b/public/app/core/directives/metric_segment.ts index 3718d7fbd4a..117f776f487 100644 --- a/public/app/core/directives/metric_segment.ts +++ b/public/app/core/directives/metric_segment.ts @@ -4,16 +4,16 @@ import coreModule from '../core_module'; /** @ngInject */ export function metricSegment($compile, $sce) { - let inputTemplate = + const inputTemplate = ''; - let linkTemplate = + const linkTemplate = ''; - let selectTemplate = + const selectTemplate = ''; @@ -25,13 +25,13 @@ export function metricSegment($compile, $sce) { debounce: '@', }, link: function($scope, elem) { - let $input = $(inputTemplate); - let segment = $scope.segment; - let $button = $(segment.selectMode ? selectTemplate : linkTemplate); + const $input = $(inputTemplate); + const segment = $scope.segment; + const $button = $(segment.selectMode ? selectTemplate : linkTemplate); let options = null; let cancelBlur = null; let linkMode = true; - let debounceLookup = $scope.debounce; + const debounceLookup = $scope.debounce; $input.appendTo(elem); $button.appendTo(elem); @@ -44,7 +44,7 @@ export function metricSegment($compile, $sce) { value = _.unescape(value); $scope.$apply(function() { - let selected = _.find($scope.altSegments, { value: value }); + const selected = _.find($scope.altSegments, { value: value }); if (selected) { segment.value = selected.value; segment.html = selected.html || selected.value; @@ -141,10 +141,10 @@ export function metricSegment($compile, $sce) { matcher: $scope.matcher, }); - let typeahead = $input.data('typeahead'); + const typeahead = $input.data('typeahead'); typeahead.lookup = function() { this.query = this.$element.val() || ''; - let items = this.source(this.query, $.proxy(this.process, this)); + const items = this.source(this.query, $.proxy(this.process, this)); return items ? this.process(items) : items; }; @@ -169,7 +169,7 @@ export function metricSegment($compile, $sce) { linkMode = false; - let typeahead = $input.data('typeahead'); + const typeahead = $input.data('typeahead'); if (typeahead) { $input.val(''); typeahead.lookup(); @@ -200,8 +200,8 @@ export function metricSegmentModel(uiSegmentSrv, $q) { let cachedOptions; $scope.valueToSegment = function(value) { - let option = _.find($scope.options, { value: value }); - let segment = { + const option = _.find($scope.options, { value: value }); + const segment = { cssClass: attrs.cssClass, custom: attrs.custom, value: option ? option.text : value, @@ -234,7 +234,7 @@ export function metricSegmentModel(uiSegmentSrv, $q) { $scope.onSegmentChange = function() { if (cachedOptions) { - let option = _.find(cachedOptions, { text: $scope.segment.value }); + const option = _.find(cachedOptions, { text: $scope.segment.value }); if (option && option.value !== $scope.property) { $scope.property = option.value; } else if (attrs.custom !== 'false') { diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 299de05f112..034b312aa0e 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -156,7 +156,7 @@ function gfDropdown($parse, $compile, $timeout) { var ul = ['']; for (let index = 0; index < items.length; index++) { - let item = items[index]; + const item = items[index]; if (item.divider) { ul.splice(index + 1, 0, '
  • '); diff --git a/public/app/core/directives/value_select_dropdown.ts b/public/app/core/directives/value_select_dropdown.ts index d384904c2d8..69504c1bb1b 100644 --- a/public/app/core/directives/value_select_dropdown.ts +++ b/public/app/core/directives/value_select_dropdown.ts @@ -46,16 +46,16 @@ export class ValueSelectDropdownCtrl { } updateLinkText() { - let current = this.variable.current; + const current = this.variable.current; if (current.tags && current.tags.length) { // filer out values that are in selected tags - let selectedAndNotInTag = _.filter(this.variable.options, option => { + const selectedAndNotInTag = _.filter(this.variable.options, option => { if (!option.selected) { return false; } for (let i = 0; i < current.tags.length; i++) { - let tag = current.tags[i]; + const tag = current.tags[i]; if (_.indexOf(tag.values, option.value) !== -1) { return false; } @@ -64,7 +64,7 @@ export class ValueSelectDropdownCtrl { }); // convert values to text - let currentTexts = _.map(selectedAndNotInTag, 'text'); + const currentTexts = _.map(selectedAndNotInTag, 'text'); // join texts this.linkText = currentTexts.join(' + '); @@ -142,7 +142,7 @@ export class ValueSelectDropdownCtrl { commitChange = commitChange || false; excludeOthers = excludeOthers || false; - let setAllExceptCurrentTo = newValue => { + const setAllExceptCurrentTo = newValue => { _.each(this.options, other => { if (option !== other) { other.selected = newValue; @@ -246,9 +246,9 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { controllerAs: 'vm', bindToController: true, link: function(scope, elem) { - let bodyEl = angular.element($window.document.body); - let linkEl = elem.find('.variable-value-link'); - let inputEl = elem.find('input'); + const bodyEl = angular.element($window.document.body); + const linkEl = elem.find('.variable-value-link'); + const inputEl = elem.find('input'); function openDropdown() { inputEl.css('width', Math.max(linkEl.width(), 80) + 'px'); @@ -288,7 +288,7 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { } }); - let cleanUp = $rootScope.$on('template-variable-value-updated', () => { + const cleanUp = $rootScope.$on('template-variable-value-updated', () => { scope.vm.updateLinkText(); }); diff --git a/public/app/core/nav_model_srv.ts b/public/app/core/nav_model_srv.ts index a9ebd4e79ed..2bed33e70da 100644 --- a/public/app/core/nav_model_srv.ts +++ b/public/app/core/nav_model_srv.ts @@ -41,14 +41,14 @@ export class NavModelSrv { var children = this.navItems; var nav = new NavModel(); - for (let id of args) { + for (const id of args) { // if its a number then it's the index to use for main if (_.isNumber(id)) { nav.main = nav.breadcrumbs[id]; break; } - let node = _.find(children, { id: id }); + const node = _.find(children, { id: id }); nav.breadcrumbs.push(node); nav.node = node; nav.main = node; @@ -56,7 +56,7 @@ export class NavModelSrv { } if (nav.main.children) { - for (let item of nav.main.children) { + for (const item of nav.main.children) { item.active = false; if (item.url === nav.node.url) { diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 1aeeedef4dd..4dd8a123378 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -276,11 +276,11 @@ export class BackendSrv { deleteFoldersAndDashboards(folderUids, dashboardUids) { const tasks = []; - for (let folderUid of folderUids) { + for (const folderUid of folderUids) { tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true)); } - for (let dashboardUid of dashboardUids) { + for (const dashboardUid of dashboardUids) { tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true)); } @@ -290,7 +290,7 @@ export class BackendSrv { moveDashboards(dashboardUids, toFolder) { const tasks = []; - for (let uid of dashboardUids) { + for (const uid of dashboardUids) { tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder)); } @@ -304,7 +304,7 @@ export class BackendSrv { } private moveDashboard(uid, toFolder) { - let deferred = this.$q.defer(); + const deferred = this.$q.defer(); this.getDashboardByUid(uid).then(fullDash => { const model = new DashboardModel(fullDash.dashboard, fullDash.meta); @@ -315,7 +315,7 @@ export class BackendSrv { } const clone = model.getSaveModelClone(); - let options = { + const options = { folderId: toFolder.id, overwrite: false, }; diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 4a5649a6c52..bdc2976a94c 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -15,7 +15,7 @@ export class BridgeSrv { init() { this.$rootScope.$on('$routeUpdate', (evt, data) => { - let angularUrl = this.$location.url(); + const angularUrl = this.$location.url(); if (store.view.currentUrl !== angularUrl) { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); } @@ -28,7 +28,7 @@ export class BridgeSrv { reaction( () => store.view.currentUrl, currentUrl => { - let angularUrl = this.$location.url(); + const angularUrl = this.$location.url(); const url = locationUtil.stripBaseFromUrl(currentUrl); if (angularUrl !== url) { this.$timeout(() => { diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index 086843b6f9a..de06daf2c1a 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -36,7 +36,7 @@ class DynamicDirectiveSrv { } create(options) { - let directiveDef = { + const directiveDef = { restrict: 'E', scope: options.scope, link: (scope, elem, attrs) => { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 9d914a94a1c..5405a347ba0 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -210,7 +210,7 @@ export class KeybindingSrv { // duplicate panel this.bind('p d', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { - let panelIndex = dashboard.getPanelInfoById(dashboard.meta.focusPanelId).index; + const panelIndex = dashboard.getPanelInfoById(dashboard.meta.focusPanelId).index; dashboard.duplicatePanel(dashboard.panels[panelIndex]); } }); diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts index 3c61412669e..aeffaaa9b3b 100644 --- a/public/app/core/services/ng_react.ts +++ b/public/app/core/services/ng_react.ts @@ -295,6 +295,6 @@ var reactDirective = function($injector) { }; }; -let ngModule = angular.module('react', []); +const ngModule = angular.module('react', []); ngModule.directive('reactComponent', ['$injector', reactComponent]); ngModule.factory('reactDirective', ['$injector', reactDirective]); diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 9f32e21f3f6..017b2c15efc 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -85,10 +85,10 @@ export class SearchSrv { } search(options) { - let sections: any = {}; - let promises = []; - let query = _.clone(options); - let hasFilters = + const sections: any = {}; + const promises = []; + const query = _.clone(options); + const hasFilters = options.query || (options.tag && options.tag.length > 0) || options.starred || @@ -124,7 +124,7 @@ export class SearchSrv { } // create folder index - for (let hit of results) { + for (const hit of results) { if (hit.type === 'dash-folder') { sections[hit.id] = { id: hit.id, @@ -140,7 +140,7 @@ export class SearchSrv { } } - for (let hit of results) { + for (const hit of results) { if (hit.type === 'dash-folder') { continue; } @@ -185,7 +185,7 @@ export class SearchSrv { return Promise.resolve(section); } - let query = { + const query = { folderIds: [section.id], }; diff --git a/public/app/core/services/segment_srv.ts b/public/app/core/services/segment_srv.ts index 042340e6102..5250febc11a 100644 --- a/public/app/core/services/segment_srv.ts +++ b/public/app/core/services/segment_srv.ts @@ -3,7 +3,7 @@ import coreModule from '../core_module'; /** @ngInject */ export function uiSegmentSrv($sce, templateSrv) { - let self = this; + const self = this; function MetricSegment(options) { if (options === '*' || options.value === '*') { @@ -78,7 +78,7 @@ export function uiSegmentSrv($sce, templateSrv) { this.transformToSegments = function(addTemplateVars, variableTypeFilter) { return function(results) { - let segments = _.map(results, function(segment) { + const segments = _.map(results, function(segment) { return self.newSegment({ value: segment.text, expandable: segment.expandable }); }); diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts index b19bd117766..e9cd5973d36 100644 --- a/public/app/core/specs/backend_srv.test.ts +++ b/public/app/core/specs/backend_srv.test.ts @@ -2,14 +2,14 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('app/core/store'); describe('backend_srv', function() { - let _httpBackend = options => { + const _httpBackend = options => { if (options.url === 'gateway-error') { return Promise.reject({ status: 502 }); } return Promise.resolve({}); }; - let _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); + const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); describe('when handling errors', () => { it('should return the http status code', async () => { diff --git a/public/app/core/specs/file_export.test.ts b/public/app/core/specs/file_export.test.ts index 915ce08fcd2..ced94fcdbc0 100644 --- a/public/app/core/specs/file_export.test.ts +++ b/public/app/core/specs/file_export.test.ts @@ -2,7 +2,7 @@ import * as fileExport from '../utils/file_export'; import { beforeEach, expect } from 'test/lib/common'; describe('file_export', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.seriesList = [ @@ -28,7 +28,7 @@ describe('file_export', () => { describe('when exporting series as rows', () => { it('should export points in proper order', () => { - let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); + const text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); const expectedText = '"Series";"Time";"Value"\r\n' + '"series_1";"1500026100";1\r\n' + @@ -48,7 +48,7 @@ describe('file_export', () => { describe('when exporting series as columns', () => { it('should export points in proper order', () => { - let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); + const text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); const expectedText = '"Time";"series_1";"series_2"\r\n' + '"1500026100";1;11\r\n' + diff --git a/public/app/core/specs/search.test.ts b/public/app/core/specs/search.test.ts index 8aea35af213..3cc789b3cc5 100644 --- a/public/app/core/specs/search.test.ts +++ b/public/app/core/specs/search.test.ts @@ -12,7 +12,7 @@ describe('SearchCtrl', () => { search: (options: any) => {}, getDashboardTags: () => {}, }; - let ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub); + const ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub); describe('Given an empty result', () => { beforeEach(() => { diff --git a/public/app/core/specs/search_results.test.ts b/public/app/core/specs/search_results.test.ts index 830496be3a8..96dbc8bb963 100644 --- a/public/app/core/specs/search_results.test.ts +++ b/public/app/core/specs/search_results.test.ts @@ -12,7 +12,7 @@ describe('SearchResultsCtrl', () => { let ctrl; describe('when checking an item that is not checked', () => { - let item = { checked: false }; + const item = { checked: false }; let selectionChanged = false; beforeEach(() => { @@ -31,7 +31,7 @@ describe('SearchResultsCtrl', () => { }); describe('when checking an item that is checked', () => { - let item = { checked: true }; + const item = { checked: true }; let selectionChanged = false; beforeEach(() => { @@ -72,7 +72,7 @@ describe('SearchResultsCtrl', () => { folderExpanded = true; }; - let folder = { + const folder = { expanded: false, toggle: () => Promise.resolve(folder), }; @@ -94,7 +94,7 @@ describe('SearchResultsCtrl', () => { folderExpanded = true; }; - let folder = { + const folder = { expanded: true, toggle: () => Promise.resolve(folder), }; diff --git a/public/app/core/specs/ticks.test.ts b/public/app/core/specs/ticks.test.ts index 8b7e0cd73b5..73d0e96cbd2 100644 --- a/public/app/core/specs/ticks.test.ts +++ b/public/app/core/specs/ticks.test.ts @@ -2,7 +2,7 @@ import * as ticks from '../utils/ticks'; describe('ticks', () => { describe('getFlotTickDecimals()', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.axis = {}; diff --git a/public/app/core/specs/time_series.test.ts b/public/app/core/specs/time_series.test.ts index bf50d807e03..35b75b3da5e 100644 --- a/public/app/core/specs/time_series.test.ts +++ b/public/app/core/specs/time_series.test.ts @@ -329,7 +329,7 @@ describe('TimeSeries', function() { describe('legend decimals', function() { let series, panel; - let height = 200; + const height = 200; beforeEach(function() { testData = { alias: 'test', @@ -348,7 +348,7 @@ describe('TimeSeries', function() { }); it('should set decimals based on Y axis (expect calculated decimals = 1)', function() { - let data = [series]; + const data = [series]; // Expect ticks with this data will have decimals = 1 updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(2); @@ -358,21 +358,21 @@ describe('TimeSeries', function() { testData.datapoints = [[10, 2], [0, 3], [100, 4], [80, 5]]; series = new TimeSeries(testData); series.getFlotPairs(); - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(0); }); it('should set decimals to Y axis decimals + 1', function() { panel.yaxes[0].decimals = 2; - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(3); }); it('should set decimals to legend decimals value if it was set explicitly', function() { panel.decimals = 3; - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(3); }); diff --git a/public/app/core/specs/value_select_dropdown.test.ts b/public/app/core/specs/value_select_dropdown.test.ts index 3cc310435b7..024774250b8 100644 --- a/public/app/core/specs/value_select_dropdown.test.ts +++ b/public/app/core/specs/value_select_dropdown.test.ts @@ -3,7 +3,7 @@ import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown'; import q from 'q'; describe('SelectDropdownCtrl', () => { - let tagValuesMap: any = {}; + const tagValuesMap: any = {}; ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn(); let ctrl; diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index f4d0943d52f..c29242c9aca 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -27,11 +27,11 @@ function translateFillOption(fill) { */ export function updateLegendValues(data: TimeSeries[], panel, height) { for (let i = 0; i < data.length; i++) { - let series = data[i]; + const series = data[i]; const yaxes = panel.yaxes; const seriesYAxis = series.yaxis || 1; const axis = yaxes[seriesYAxis - 1]; - let formater = kbn.valueFormats[axis.format]; + const formater = kbn.valueFormats[axis.format]; // decimal override if (_.isNumber(panel.decimals)) { @@ -54,7 +54,7 @@ export function getDataMinMax(data: TimeSeries[]) { let datamin = null; let datamax = null; - for (let series of data) { + for (const series of data) { if (datamax === null || datamax < series.stats.max) { datamax = series.stats.max; } @@ -225,7 +225,7 @@ export default class TimeSeries { // Due to missing values we could have different timeStep all along the series // so we have to find the minimum one (could occur with aggregators such as ZimSum) if (previousTime !== undefined) { - let timeStep = currentTime - previousTime; + const timeStep = currentTime - previousTime; if (timeStep < this.stats.timeStep) { this.stats.timeStep = timeStep; } diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index 8a70e093ea2..e8a7366beb5 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -9,7 +9,7 @@ export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; export const REGION_FILL_ALPHA = 0.09; -let colors = [ +const colors = [ '#7EB26D', '#EAB839', '#6ED0E0', @@ -69,7 +69,7 @@ let colors = [ ]; export function sortColorsByHue(hexColors) { - let hslColors = _.map(hexColors, hexToHsl); + const hslColors = _.map(hexColors, hexToHsl); let sortedHSLColors = _.sortBy(hslColors, ['h']); sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); diff --git a/public/app/core/utils/dag.test.ts b/public/app/core/utils/dag.test.ts index a89ab27cda3..064da13806b 100644 --- a/public/app/core/utils/dag.test.ts +++ b/public/app/core/utils/dag.test.ts @@ -2,16 +2,16 @@ 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'); + const dag = new Graph(); + const nodeA = dag.createNode('A'); + const nodeB = dag.createNode('B'); + const nodeC = dag.createNode('C'); + const nodeD = dag.createNode('D'); + const nodeE = dag.createNode('E'); + const nodeF = dag.createNode('F'); + const nodeG = dag.createNode('G'); + const nodeH = dag.createNode('H'); + const 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); diff --git a/public/app/core/utils/dag.ts b/public/app/core/utils/dag.ts index 1d61280fb05..eb7ff1c3b1a 100644 --- a/public/app/core/utils/dag.ts +++ b/public/app/core/utils/dag.ts @@ -26,8 +26,8 @@ export class Edge { unlink() { let pos; - let inode = this.inputNode; - let onode = this.outputNode; + const inode = this.inputNode; + const onode = this.outputNode; if (!(inode && onode)) { return; @@ -96,12 +96,12 @@ export class Node { } getOptimizedInputEdges(): Edge[] { - let toBeRemoved = []; + const toBeRemoved = []; this.inputEdges.forEach(e => { - let inputEdgesNodes = e.inputNode.inputEdges.map(e => e.inputNode); + const inputEdgesNodes = e.inputNode.inputEdges.map(e => e.inputNode); inputEdgesNodes.forEach(n => { - let edgeToRemove = n.getEdgeTo(this.name); + const edgeToRemove = n.getEdgeTo(this.name); if (edgeToRemove) { toBeRemoved.push(edgeToRemove); } @@ -124,7 +124,7 @@ export class Graph { } createNodes(names: string[]): Node[] { - let nodes = []; + const nodes = []; names.forEach(name => { nodes.push(this.createNode(name)); }); @@ -134,8 +134,8 @@ export class Graph { link(input: string | string[] | Node | Node[], output: string | string[] | Node | Node[]): Edge[] { let inputArr = []; let outputArr = []; - let inputNodes = []; - let outputNodes = []; + const inputNodes = []; + const outputNodes = []; if (input instanceof Array) { inputArr = input; @@ -167,7 +167,7 @@ export class Graph { } } - let edges = []; + const edges = []; inputNodes.forEach(input => { outputNodes.forEach(output => { edges.push(this.createEdge().link(input, output)); diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index f25d340a0be..298a06c64fd 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -74,7 +74,7 @@ export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATE } export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - let text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); + const text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); saveSaveBlob(text, EXPORT_FILENAME); } @@ -115,7 +115,7 @@ export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAU function mergeSeriesByTime(seriesList) { let timestamps = []; for (let i = 0; i < seriesList.length; i++) { - let seriesPoints = seriesList[i].datapoints; + const seriesPoints = seriesList[i].datapoints; for (let j = 0; j < seriesPoints.length; j++) { timestamps.push(seriesPoints[j][POINT_TIME_INDEX]); } @@ -123,9 +123,9 @@ function mergeSeriesByTime(seriesList) { timestamps = sortedUniq(timestamps.sort()); for (let i = 0; i < seriesList.length; i++) { - let seriesPoints = seriesList[i].datapoints; - let seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]); - let extendedSeries = []; + const seriesPoints = seriesList[i].datapoints; + const seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]); + const extendedSeries = []; let pointIndex; for (let j = 0; j < timestamps.length; j++) { pointIndex = sortedIndexOf(seriesTimestamps, timestamps[j]); @@ -141,7 +141,7 @@ function mergeSeriesByTime(seriesList) { } export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); + const text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); saveSaveBlob(text, EXPORT_FILENAME); } @@ -157,11 +157,11 @@ export function convertTableDataToCsv(table, excel = false) { } export function exportTableDataToCsv(table, excel = false) { - let text = convertTableDataToCsv(table, excel); + const text = convertTableDataToCsv(table, excel); saveSaveBlob(text, EXPORT_FILENAME); } export function saveSaveBlob(payload, fname) { - let blob = new Blob([payload], { type: 'text/csv;charset=utf-8;header=present;' }); + const blob = new Blob([payload], { type: 'text/csv;charset=utf-8;header=present;' }); saveAs(blob, fname); } diff --git a/public/app/core/utils/outline.ts b/public/app/core/utils/outline.ts index 94393e781e9..cc06102bfdc 100644 --- a/public/app/core/utils/outline.ts +++ b/public/app/core/utils/outline.ts @@ -1,6 +1,6 @@ // based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/ function outlineFixer() { - let d: any = document; + const d: any = document; var style_element = d.createElement('STYLE'); var dom_events = 'addEventListener' in d; diff --git a/public/app/core/utils/rangeutil.ts b/public/app/core/utils/rangeutil.ts index 95cfe42f0b8..8e0f87df686 100644 --- a/public/app/core/utils/rangeutil.ts +++ b/public/app/core/utils/rangeutil.ts @@ -92,7 +92,7 @@ function formatDate(date) { // now/d // if no to then to now is assumed export function describeTextRange(expr: any) { - let isLast = expr.indexOf('+') !== 0; + const isLast = expr.indexOf('+') !== 0; if (expr.indexOf('now') === -1) { expr = (isLast ? 'now-' : 'now') + expr; } @@ -108,11 +108,11 @@ export function describeTextRange(expr: any) { opt = { from: 'now', to: expr }; } - let parts = /^now([-+])(\d+)(\w)/.exec(expr); + const parts = /^now([-+])(\d+)(\w)/.exec(expr); if (parts) { - let unit = parts[3]; - let amount = parseInt(parts[2]); - let span = spans[unit]; + const unit = parts[3]; + const amount = parseInt(parts[2]); + const span = spans[unit]; if (span) { opt.display = isLast ? 'Last ' : 'Next '; opt.display += amount + ' ' + span.display; diff --git a/public/app/core/utils/sort_by_keys.ts b/public/app/core/utils/sort_by_keys.ts index 9dff252576a..0020d04f290 100644 --- a/public/app/core/utils/sort_by_keys.ts +++ b/public/app/core/utils/sort_by_keys.ts @@ -7,7 +7,7 @@ export default function sortByKeys(input) { if (_.isPlainObject(input)) { var sortedObject = {}; - for (let key of _.keys(input).sort()) { + for (const key of _.keys(input).sort()) { sortedObject[key] = sortByKeys(input[key]); } return sortedObject; diff --git a/public/app/core/utils/tags.ts b/public/app/core/utils/tags.ts index 678fd8c94be..d0f244be76b 100644 --- a/public/app/core/utils/tags.ts +++ b/public/app/core/utils/tags.ts @@ -67,9 +67,9 @@ const TAG_BORDER_COLORS = [ * @param name tag name */ export function getTagColorsFromName(name: string): { color: string; borderColor: string } { - let hash = djb2(name.toLowerCase()); - let color = TAG_COLORS[Math.abs(hash % TAG_COLORS.length)]; - let borderColor = TAG_BORDER_COLORS[Math.abs(hash % TAG_BORDER_COLORS.length)]; + const hash = djb2(name.toLowerCase()); + const color = TAG_COLORS[Math.abs(hash % TAG_COLORS.length)]; + const borderColor = TAG_BORDER_COLORS[Math.abs(hash % TAG_BORDER_COLORS.length)]; return { color, borderColor }; } diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index 66e6a7ce4fc..d87dedccab1 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -7,7 +7,7 @@ * @param count Ticks count */ export function tickStep(start: number, stop: number, count: number): number { - let e10 = Math.sqrt(50), + const e10 = Math.sqrt(50), e5 = Math.sqrt(10), e2 = Math.sqrt(2); @@ -76,7 +76,7 @@ export function getFlotRange(panelMin, panelMax, datamin, datamax) { let min = +(panelMin != null ? panelMin : datamin); let max = +(panelMax != null ? panelMax : datamax); - let delta = max - min; + const delta = max - min; if (delta === 0.0) { // Grafana fix: wide Y min and max using increased wideFactor @@ -123,11 +123,11 @@ export function getFlotTickDecimals(datamin, datamax, axis, height) { const { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax); const noTicks = 0.3 * Math.sqrt(height); const delta = (max - min) / noTicks; - let dec = -Math.floor(Math.log(delta) / Math.LN10); + const dec = -Math.floor(Math.log(delta) / Math.LN10); - let magn = Math.pow(10, -dec); + const magn = Math.pow(10, -dec); // norm is between 1.0 and 10.0 - let norm = delta / magn; + const norm = delta / magn; let size; if (norm < 1.5) { @@ -159,10 +159,10 @@ export function getFlotTickDecimals(datamin, datamax, axis, height) { */ export function grafanaTimeFormat(ticks, min, max) { if (min && max && ticks) { - let range = max - min; - let secPerTick = range / ticks / 1000; - let oneDay = 86400000; - let oneYear = 31536000000; + const range = max - min; + const secPerTick = range / ticks / 1000; + const oneDay = 86400000; + const oneYear = 31536000000; if (secPerTick <= 45) { return '%H:%M:%S'; @@ -193,7 +193,7 @@ export function logp(value, base) { * Get decimal precision of number (3.14 => 2) */ export function getPrecision(num: number): number { - let str = num.toString(); + const str = num.toString(); return getStringPrecision(str); } @@ -201,7 +201,7 @@ export function getPrecision(num: number): number { * Get decimal precision of number stored as a string ("3.14" => 2) */ export function getStringPrecision(num: string): number { - let dot_index = num.indexOf('.'); + const dot_index = num.indexOf('.'); if (dot_index === -1) { return 0; } else { diff --git a/public/app/core/utils/url.ts b/public/app/core/utils/url.ts index b57d5721d57..857e76d9094 100644 --- a/public/app/core/utils/url.ts +++ b/public/app/core/utils/url.ts @@ -3,14 +3,14 @@ */ export function toUrlParams(a) { - let s = []; - let rbracket = /\[\]$/; + const s = []; + const rbracket = /\[\]$/; - let isArray = function(obj) { + const isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; - let add = function(k, v) { + const add = function(k, v) { v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v; if (typeof v !== 'boolean') { s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v); @@ -19,7 +19,7 @@ export function toUrlParams(a) { } }; - let buildParams = function(prefix, obj) { + const buildParams = function(prefix, obj) { var i, len, key; if (prefix) { diff --git a/public/app/core/utils/version.ts b/public/app/core/utils/version.ts index 6ee1400df51..8b249563d86 100644 --- a/public/app/core/utils/version.ts +++ b/public/app/core/utils/version.ts @@ -9,7 +9,7 @@ export class SemVersion { meta: string; constructor(version: string) { - let match = versionPattern.exec(version); + const match = versionPattern.exec(version); if (match) { this.major = Number(match[1]); this.minor = Number(match[2] || 0); @@ -19,7 +19,7 @@ export class SemVersion { } isGtOrEq(version: string): boolean { - let compared = new SemVersion(version); + const compared = new SemVersion(version); return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch); } @@ -29,6 +29,6 @@ export class SemVersion { } export function isVersionGtOrEq(a: string, b: string): boolean { - let a_semver = new SemVersion(a); + const a_semver = new SemVersion(a); return a_semver.isGtOrEq(b); } diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 79baa1e3f5a..a25d37913d4 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -184,7 +184,7 @@ export class AlertTabCtrl { ThresholdMapper.alertToGraphThresholds(this.panel); - for (let addedNotification of alert.notifications) { + for (const addedNotification of alert.notifications) { var model = _.find(this.notifications, { id: addedNotification.id }); if (model && model.isDefault === false) { model.iconClass = this.getNotificationIcon(model.type); @@ -192,7 +192,7 @@ export class AlertTabCtrl { } } - for (let notification of this.notifications) { + for (const notification of this.notifications) { if (notification.isDefault) { notification.iconClass = this.getNotificationIcon(notification.type); notification.bgColor = '#00678b'; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 18b1c4d1d55..eb14766d1fb 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -30,7 +30,7 @@ export class AlertNotificationEditCtrl { this.notifiers = notifiers; // add option templates - for (let notifier of this.notifiers) { + for (const notifier of this.notifiers) { this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate); } diff --git a/public/app/features/alerting/threshold_mapper.ts b/public/app/features/alerting/threshold_mapper.ts index 9142c74b6e3..50324dc18ca 100644 --- a/public/app/features/alerting/threshold_mapper.ts +++ b/public/app/features/alerting/threshold_mapper.ts @@ -1,7 +1,7 @@ export class ThresholdMapper { static alertToGraphThresholds(panel) { for (var i = 0; i < panel.alert.conditions.length; i++) { - let condition = panel.alert.conditions[i]; + const condition = panel.alert.conditions[i]; if (condition.type !== 'query') { continue; } @@ -11,18 +11,18 @@ export class ThresholdMapper { switch (evaluator.type) { case 'gt': { - let value = evaluator.params[0]; + const value = evaluator.params[0]; thresholds.push({ value: value, op: 'gt' }); break; } case 'lt': { - let value = evaluator.params[0]; + const value = evaluator.params[0]; thresholds.push({ value: value, op: 'lt' }); break; } case 'outside_range': { - let value1 = evaluator.params[0]; - let value2 = evaluator.params[1]; + const value1 = evaluator.params[0]; + const value2 = evaluator.params[1]; if (value1 > value2) { thresholds.push({ value: value1, op: 'gt' }); @@ -35,8 +35,8 @@ export class ThresholdMapper { break; } case 'within_range': { - let value1 = evaluator.params[0]; - let value2 = evaluator.params[1]; + const value1 = evaluator.params[0]; + const value2 = evaluator.params[1]; if (value1 > value2) { thresholds.push({ value: value1, op: 'lt' }); diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index 5578a979146..b8def36829a 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -91,7 +91,7 @@ export class AnnotationsSrv { var range = this.timeSrv.timeRange(); var promises = []; - for (let annotation of dashboard.annotations.list) { + for (const annotation of dashboard.annotations.list) { if (!annotation.enable) { continue; } diff --git a/public/app/features/annotations/event_editor.ts b/public/app/features/annotations/event_editor.ts index 1f94e978029..90c425438ab 100644 --- a/public/app/features/annotations/event_editor.ts +++ b/public/app/features/annotations/event_editor.ts @@ -31,7 +31,7 @@ export class EventEditorCtrl { return; } - let saveModel = _.cloneDeep(this.event); + const saveModel = _.cloneDeep(this.event); saveModel.time = saveModel.time.valueOf(); saveModel.timeEnd = 0; @@ -85,7 +85,7 @@ export class EventEditorCtrl { function tryEpochToMoment(timestamp) { if (timestamp && _.isNumber(timestamp)) { - let epoch = Number(timestamp); + const epoch = Number(timestamp); return moment(epoch); } else { return timestamp; diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index 7db6a19f2c6..a6fceac2e54 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -125,11 +125,11 @@ export class EventManager { } } - let regions = getRegions(annotations); + const regions = getRegions(annotations); addRegionMarking(regions, flotOptions); - let eventSectionHeight = 20; - let eventSectionMargin = 7; + const eventSectionHeight = 20; + const eventSectionMargin = 7; flotOptions.grid.eventSectionHeight = eventSectionMargin; flotOptions.xaxis.eventSectionHeight = eventSectionHeight; @@ -147,8 +147,8 @@ function getRegions(events) { } function addRegionMarking(regions, flotOptions) { - let markings = flotOptions.grid.markings; - let defaultColor = DEFAULT_ANNOTATION_COLOR; + const markings = flotOptions.grid.markings; + const defaultColor = DEFAULT_ANNOTATION_COLOR; let fillColor; _.each(regions, region => { @@ -167,7 +167,7 @@ function addRegionMarking(regions, flotOptions) { } function addAlphaToRGB(colorString: string, alpha: number): string { - let color = tinycolor(colorString); + const color = tinycolor(colorString); if (color.isValid()) { color.setAlpha(alpha); return color.toRgbString(); diff --git a/public/app/features/annotations/events_processing.ts b/public/app/features/annotations/events_processing.ts index 667285d7d43..6e610fb1457 100644 --- a/public/app/features/annotations/events_processing.ts +++ b/public/app/features/annotations/events_processing.ts @@ -7,20 +7,20 @@ import _ from 'lodash'; * @param options */ export function makeRegions(annotations, options) { - let [regionEvents, singleEvents] = _.partition(annotations, 'regionId'); - let regions = getRegions(regionEvents, options.range); + const [regionEvents, singleEvents] = _.partition(annotations, 'regionId'); + const regions = getRegions(regionEvents, options.range); annotations = _.concat(regions, singleEvents); return annotations; } function getRegions(events, range) { - let region_events = _.filter(events, event => { + const region_events = _.filter(events, event => { return event.regionId; }); let regions = _.groupBy(region_events, 'regionId'); regions = _.compact( _.map(regions, region_events => { - let region_obj = _.head(region_events); + const region_obj = _.head(region_events); if (region_events && region_events.length > 1) { region_obj.timeEnd = region_events[1].time; region_obj.isRegion = true; @@ -57,9 +57,9 @@ export function dedupAnnotations(annotations) { let dedup = []; // Split events by annotationId property existence - let events = _.partition(annotations, 'id'); + const events = _.partition(annotations, 'id'); - let eventsById = _.groupBy(events[0], 'id'); + const eventsById = _.groupBy(events[0], 'id'); dedup = _.map(eventsById, eventGroup => { if (eventGroup.length > 1 && !_.every(eventGroup, isPanelAlert)) { // Get first non-panel alert diff --git a/public/app/features/annotations/specs/annotations_srv.test.ts b/public/app/features/annotations/specs/annotations_srv.test.ts index 7db7b6c9f05..97696767536 100644 --- a/public/app/features/annotations/specs/annotations_srv.test.ts +++ b/public/app/features/annotations/specs/annotations_srv.test.ts @@ -3,7 +3,7 @@ import 'app/features/dashboard/time_srv'; import { AnnotationsSrv } from '../annotations_srv'; describe('AnnotationsSrv', function() { - let $rootScope = { + const $rootScope = { onAppEvent: jest.fn(), }; let $q; @@ -11,7 +11,7 @@ describe('AnnotationsSrv', function() { let backendSrv; let timeSrv; - let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); + const annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); describe('When translating the query result', () => { const annotationSource = { diff --git a/public/app/features/annotations/specs/annotations_srv_specs.test.ts b/public/app/features/annotations/specs/annotations_srv_specs.test.ts index 35def83e4a9..49457f34d05 100644 --- a/public/app/features/annotations/specs/annotations_srv_specs.test.ts +++ b/public/app/features/annotations/specs/annotations_srv_specs.test.ts @@ -24,7 +24,7 @@ describe('Annotations', () => { { id: 2, time: 2 }, ]; - let regions = makeRegions(testAnnotations, { range: range }); + const regions = makeRegions(testAnnotations, { range: range }); expect(regions).toEqual(expectedAnnotations); }); @@ -33,7 +33,7 @@ describe('Annotations', () => { testAnnotations = [{ id: 5, time: 4, regionId: 5 }]; const expectedAnnotations = [{ id: 5, regionId: 5, isRegion: true, time: 4, timeEnd: 7 }]; - let regions = makeRegions(testAnnotations, { range: range }); + const regions = makeRegions(testAnnotations, { range: range }); expect(regions).toEqual(expectedAnnotations); }); }); @@ -49,7 +49,7 @@ describe('Annotations', () => { ]; const expectedAnnotations = [{ id: 1, time: 1 }, { id: 2, time: 2 }, { id: 5, time: 5 }]; - let deduplicated = dedupAnnotations(testAnnotations); + const deduplicated = dedupAnnotations(testAnnotations); expect(deduplicated).toEqual(expectedAnnotations); }); @@ -63,7 +63,7 @@ describe('Annotations', () => { ]; const expectedAnnotations = [{ id: 1, time: 1 }, { id: 2, time: 2 }, { id: 5, time: 5 }]; - let deduplicated = dedupAnnotations(testAnnotations); + const deduplicated = dedupAnnotations(testAnnotations); expect(deduplicated).toEqual(expectedAnnotations); }); }); diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index ee57db23675..412761dc716 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -30,7 +30,7 @@ export class AdHocFiltersCtrl { if (this.variable.value && !_.isArray(this.variable.value)) { } - for (let tag of this.variable.filters) { + for (const tag of this.variable.filters) { if (this.segments.length > 0) { this.segments.push(this.uiSegmentSrv.newCondition('AND')); } diff --git a/public/app/features/dashboard/change_tracker.ts b/public/app/features/dashboard/change_tracker.ts index 745b76ce347..1417510bb2c 100644 --- a/public/app/features/dashboard/change_tracker.ts +++ b/public/app/features/dashboard/change_tracker.ts @@ -94,13 +94,13 @@ export class ChangeTracker { // remove stuff that should not count in diff cleanDashboardFromIgnoredChanges(dashData) { // need to new up the domain model class to get access to expand / collapse row logic - let model = new DashboardModel(dashData); + const model = new DashboardModel(dashData); // Expand all rows before making comparison. This is required because row expand / collapse // change order of panel array and panel positions. model.expandRows(); - let dash = model.getSaveModelClone(); + const dash = model.getSaveModelClone(); // ignore time and refresh dash.time = 0; @@ -138,8 +138,8 @@ export class ChangeTracker { } hasChanges() { - let current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone()); - let original = this.cleanDashboardFromIgnoredChanges(this.original); + const current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone()); + const original = this.cleanDashboardFromIgnoredChanges(this.original); var currentTimepicker = _.find(current.nav, { type: 'timepicker' }); var originalTimepicker = _.find(original.nav, { type: 'timepicker' }); diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index 73e9e316b4e..b70a1847602 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -51,7 +51,7 @@ export class DashboardImportCtrl { this.inputs = []; if (this.dash.__inputs) { - for (let input of this.dash.__inputs) { + for (const input of this.dash.__inputs) { var inputModel = { name: input.name, label: input.label, @@ -95,7 +95,7 @@ export class DashboardImportCtrl { inputValueChanged() { this.inputsValid = true; - for (let input of this.inputs) { + for (const input of this.inputs) { if (!input.value) { this.inputsValid = false; } diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index 1d319929bfd..3753cbe7c55 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -389,7 +389,7 @@ export class DashboardMigrator { upgradeToGridLayout(old) { let yPos = 0; - let widthFactor = GRID_COLUMN_COUNT / 12; + const widthFactor = GRID_COLUMN_COUNT / 12; const maxPanelId = _.max( _.flattenDeep( @@ -407,15 +407,15 @@ export class DashboardMigrator { // Add special "row" panels if even one row is collapsed, repeated or has visible title const showRows = _.some(old.rows, row => row.collapse || row.showTitle || row.repeat); - for (let row of old.rows) { + for (const row of old.rows) { if (row.repeatIteration) { continue; } - let height: any = row.height || DEFAULT_ROW_HEIGHT; + const height: any = row.height || DEFAULT_ROW_HEIGHT; const rowGridHeight = getGridHeight(height); - let rowPanel: any = {}; + const rowPanel: any = {}; let rowPanelModel: PanelModel; if (showRows) { // add special row panel @@ -436,9 +436,9 @@ export class DashboardMigrator { yPos++; } - let rowArea = new RowArea(rowGridHeight, GRID_COLUMN_COUNT, yPos); + const rowArea = new RowArea(rowGridHeight, GRID_COLUMN_COUNT, yPos); - for (let panel of row.panels) { + for (const panel of row.panels) { panel.span = panel.span || DEFAULT_PANEL_SPAN; if (panel.minSpan) { panel.minSpan = Math.min(GRID_COLUMN_COUNT, GRID_COLUMN_COUNT / 12 * panel.minSpan); @@ -446,7 +446,7 @@ export class DashboardMigrator { const panelWidth = Math.floor(panel.span) * widthFactor; const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight; - let panelPos = rowArea.getPanelPosition(panelHeight, panelWidth); + const panelPos = rowArea.getPanelPosition(panelHeight, panelWidth); yPos = rowArea.yPos; panel.gridPos = { x: panelPos.x, diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 92392fc80e8..8f61cf06c60 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -95,7 +95,7 @@ export class DashboardModel { addBuiltInAnnotationQuery() { let found = false; - for (let item of this.annotations.list) { + for (const item of this.annotations.list) { if (item.builtIn === 1) { found = true; break; @@ -138,7 +138,7 @@ export class DashboardModel { // cleans meta data and other non persistent state getSaveModelClone(options?) { - let defaults = _.defaults(options || {}, { + const defaults = _.defaults(options || {}, { saveVariables: true, saveTimerange: true, }); @@ -160,8 +160,8 @@ export class DashboardModel { if (!defaults.saveVariables) { for (let i = 0; i < copy.templating.list.length; i++) { - let current = copy.templating.list[i]; - let original = _.find(this.originalTemplating, { name: current.name, type: current.type }); + const current = copy.templating.list[i]; + const original = _.find(this.originalTemplating, { name: current.name, type: current.type }); if (!original) { continue; @@ -213,13 +213,13 @@ export class DashboardModel { getNextPanelId() { let max = 0; - for (let panel of this.panels) { + for (const panel of this.panels) { if (panel.id > max) { max = panel.id; } if (panel.collapsed) { - for (let rowPanel of panel.panels) { + for (const rowPanel of panel.panels) { if (rowPanel.id > max) { max = rowPanel.id; } @@ -237,7 +237,7 @@ export class DashboardModel { } getPanelById(id) { - for (let panel of this.panels) { + for (const panel of this.panels) { if (panel.id === id) { return panel; } @@ -248,7 +248,7 @@ export class DashboardModel { addPanel(panelData) { panelData.id = this.getNextPanelId(); - let panel = new PanelModel(panelData); + const panel = new PanelModel(panelData); this.panels.unshift(panel); @@ -273,15 +273,15 @@ export class DashboardModel { } this.iteration = (this.iteration || new Date().getTime()) + 1; - let panelsToRemove = []; + const panelsToRemove = []; // cleanup scopedVars - for (let panel of this.panels) { + for (const panel of this.panels) { delete panel.scopedVars; } for (let i = 0; i < this.panels.length; i++) { - let panel = this.panels[i]; + const panel = this.panels[i]; if ((!panel.repeat || panel.repeatedByRow) && panel.repeatPanelId && panel.repeatIteration !== this.iteration) { panelsToRemove.push(panel); } @@ -304,7 +304,7 @@ export class DashboardModel { this.iteration = (this.iteration || new Date().getTime()) + 1; for (let i = 0; i < this.panels.length; i++) { - let panel = this.panels[i]; + const panel = this.panels[i]; if (panel.repeat) { this.repeatPanel(panel, i); } @@ -315,9 +315,9 @@ export class DashboardModel { } cleanUpRowRepeats(rowPanels) { - let panelsToRemove = []; + const panelsToRemove = []; for (let i = 0; i < rowPanels.length; i++) { - let panel = rowPanels[i]; + const panel = rowPanels[i]; if (!panel.repeat && panel.repeatPanelId) { panelsToRemove.push(panel); } @@ -333,16 +333,16 @@ export class DashboardModel { let rowPanels = row.panels; if (!row.collapsed) { - let rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id); + const rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id); rowPanels = this.getRowPanels(rowPanelIndex); } this.cleanUpRowRepeats(rowPanels); for (let i = 0; i < rowPanels.length; i++) { - let panel = rowPanels[i]; + const panel = rowPanels[i]; if (panel.repeat) { - let panelIndex = _.findIndex(this.panels, p => p.id === panel.id); + const panelIndex = _.findIndex(this.panels, p => p.id === panel.id); this.repeatPanel(panel, panelIndex); } } @@ -354,7 +354,7 @@ export class DashboardModel { return sourcePanel; } - let clone = new PanelModel(sourcePanel.getSaveModel()); + const clone = new PanelModel(sourcePanel.getSaveModel()); clone.id = this.getNextPanelId(); // insert after source panel + value index @@ -370,13 +370,13 @@ export class DashboardModel { // if first clone return source if (valueIndex === 0) { if (!sourceRowPanel.collapsed) { - let rowPanels = this.getRowPanels(sourcePanelIndex); + const rowPanels = this.getRowPanels(sourcePanelIndex); sourceRowPanel.panels = rowPanels; } return sourceRowPanel; } - let clone = new PanelModel(sourceRowPanel.getSaveModel()); + const clone = new PanelModel(sourceRowPanel.getSaveModel()); // for row clones we need to figure out panels under row to clone and where to insert clone let rowPanels, insertPos; if (sourceRowPanel.collapsed) { @@ -397,7 +397,7 @@ export class DashboardModel { } repeatPanel(panel: PanelModel, panelIndex: number) { - let variable = _.find(this.templating.list, { name: panel.repeat }); + const variable = _.find(this.templating.list, { name: panel.repeat }); if (!variable) { return; } @@ -407,13 +407,13 @@ export class DashboardModel { return; } - let selectedOptions = this.getSelectedVariableOptions(variable); - let minWidth = panel.minSpan || 6; + const selectedOptions = this.getSelectedVariableOptions(variable); + const minWidth = panel.minSpan || 6; let xPos = 0; let yPos = panel.gridPos.y; for (let index = 0; index < selectedOptions.length; index++) { - let option = selectedOptions[index]; + const option = selectedOptions[index]; let copy; copy = this.getPanelRepeatClone(panel, index, panelIndex); @@ -443,9 +443,9 @@ export class DashboardModel { } // Update gridPos for panels below - let yOffset = yPos - panel.gridPos.y; + const yOffset = yPos - panel.gridPos.y; if (yOffset > 0) { - let panelBelowIndex = panelIndex + selectedOptions.length; + const panelBelowIndex = panelIndex + selectedOptions.length; for (let i = panelBelowIndex; i < this.panels.length; i++) { this.panels[i].gridPos.y += yOffset; } @@ -453,7 +453,7 @@ export class DashboardModel { } repeatRow(panel: PanelModel, panelIndex: number, variable) { - let selectedOptions = this.getSelectedVariableOptions(variable); + const selectedOptions = this.getSelectedVariableOptions(variable); let yPos = panel.gridPos.y; function setScopedVars(panel, variableOption) { @@ -462,12 +462,12 @@ export class DashboardModel { } for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) { - let option = selectedOptions[optionIndex]; - let rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex); + const option = selectedOptions[optionIndex]; + const rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex); setScopedVars(rowCopy, option); - let rowHeight = this.getRowHeight(rowCopy); - let rowPanels = rowCopy.panels || []; + const rowHeight = this.getRowHeight(rowCopy); + const rowPanels = rowCopy.panels || []; let panelBelowIndex; if (panel.collapsed) { @@ -483,11 +483,11 @@ export class DashboardModel { panelBelowIndex = panelIndex + optionIndex + 1; } else { // insert after 'row' panel - let insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1; + const insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1; _.each(rowPanels, (rowPanel, i) => { setScopedVars(rowPanel, option); if (optionIndex > 0) { - let cloneRowPanel = new PanelModel(rowPanel); + const cloneRowPanel = new PanelModel(rowPanel); this.updateRepeatedPanelIds(cloneRowPanel, true); // For exposed row additionally set proper Y grid position and add it to dashboard panels cloneRowPanel.gridPos.y += rowHeight * optionIndex; @@ -650,29 +650,29 @@ export class DashboardModel { formatDate(date, format?) { date = moment.isMoment(date) ? date : moment(date); format = format || 'YYYY-MM-DD HH:mm:ss'; - let timezone = this.getTimezone(); + const timezone = this.getTimezone(); return timezone === 'browser' ? moment(date).format(format) : moment.utc(date).format(format); } destroy() { this.events.removeAllListeners(); - for (let panel of this.panels) { + for (const panel of this.panels) { panel.destroy(); } } toggleRow(row: PanelModel) { - let rowIndex = _.indexOf(this.panels, row); + const rowIndex = _.indexOf(this.panels, row); if (row.collapsed) { row.collapsed = false; - let hasRepeat = _.some(row.panels, p => p.repeat); + const hasRepeat = _.some(row.panels, p => p.repeat); if (row.panels.length > 0) { // Use first panel to figure out if it was moved or pushed - let firstPanel = row.panels[0]; - let yDiff = firstPanel.gridPos.y - (row.gridPos.y + row.gridPos.h); + const firstPanel = row.panels[0]; + const yDiff = firstPanel.gridPos.y - (row.gridPos.y + row.gridPos.h); // start inserting after row let insertPos = rowIndex + 1; @@ -680,7 +680,7 @@ export class DashboardModel { // needed to know home much panels below should be pushed down let yMax = row.gridPos.y; - for (let panel of row.panels) { + for (const panel of row.panels) { // make sure y is adjusted (in case row moved while collapsed) // console.log('yDiff', yDiff); panel.gridPos.y -= yDiff; @@ -713,7 +713,7 @@ export class DashboardModel { return; } - let rowPanels = this.getRowPanels(rowIndex); + const rowPanels = this.getRowPanels(rowIndex); // remove panels _.pull(this.panels, ...rowPanels); @@ -729,10 +729,10 @@ export class DashboardModel { * Will return all panels after rowIndex until it encounters another row */ getRowPanels(rowIndex: number): PanelModel[] { - let rowPanels = []; + const rowPanels = []; for (let index = rowIndex + 1; index < this.panels.length; index++) { - let panel = this.panels[index]; + const panel = this.panels[index]; // break when encountering another row if (panel.type === 'row') { @@ -791,7 +791,7 @@ export class DashboardModel { } private updateSchema(old) { - let migrator = new DashboardMigrator(this); + const migrator = new DashboardMigrator(this); migrator.updateSchema(old); } diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index f1f2290ce40..9459fc41753 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -68,18 +68,18 @@ export class AddPanelPanel extends React.Component item) .value(); let copiedPanels = []; - let copiedPanelJson = store.get(LS_PANEL_COPY_KEY); + const copiedPanelJson = store.get(LS_PANEL_COPY_KEY); if (copiedPanelJson) { - let copiedPanel = JSON.parse(copiedPanelJson); - let pluginInfo = _.find(panels, { id: copiedPanel.type }); + const copiedPanel = JSON.parse(copiedPanelJson); + const pluginInfo = _.find(panels, { id: copiedPanel.type }); if (pluginInfo) { - let pluginCopy = _.cloneDeep(pluginInfo); + const pluginCopy = _.cloneDeep(pluginInfo); pluginCopy.name = copiedPanel.title; pluginCopy.sort = -1; pluginCopy.defaults = copiedPanel; @@ -129,7 +129,7 @@ export class AddPanelPanel extends React.Component; } @@ -156,7 +156,7 @@ export class AddPanelPanel extends React.Component { return regex.test(panel.name); }); @@ -189,12 +189,12 @@ export class AddPanelPanel extends React.Component { const layout = []; this.panelMap = {}; - for (let panel of this.dashboard.panels) { - let stringId = panel.id.toString(); + for (const panel of this.dashboard.panels) { + const stringId = panel.id.toString(); this.panelMap[stringId] = panel; if (!panel.gridPos) { @@ -103,7 +103,7 @@ export class DashboardGrid extends React.Component { continue; } - let panelPos: any = { + const panelPos: any = { i: stringId, x: panel.gridPos.x, y: panel.gridPos.y, @@ -174,7 +174,7 @@ export class DashboardGrid extends React.Component { renderPanels() { const panelElements = []; - for (let panel of this.dashboard.panels) { + for (const panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
    diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 628f09349d3..a83efe7d390 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -22,7 +22,7 @@ export class DashNavCtrl { } toggleSettings() { - let search = this.$location.search(); + const search = this.$location.search(); if (search.editview) { delete search.editview; } else { @@ -32,7 +32,7 @@ export class DashNavCtrl { } close() { - let search = this.$location.search(); + const search = this.$location.search(); if (search.editview) { delete search.editview; } else if (search.fullscreen) { diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 2e61ce9f8a8..d314f13be4b 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -29,7 +29,7 @@ export class DashExportCtrl { saveJson() { var clone = this.dash; - let editScope = this.$rootScope.$new(); + const editScope = this.$rootScope.$new(); editScope.object = clone; editScope.enableCopy = true; diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts index fc24de76fcc..91e9f12ae54 100644 --- a/public/app/features/dashboard/export/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -24,7 +24,7 @@ export class DashboardExporter { var promises = []; var variableLookup: any = {}; - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { variableLookup[variable.name] = variable; } @@ -69,7 +69,7 @@ export class DashboardExporter { } if (panel.targets) { - for (let target of panel.targets) { + for (const target of panel.targets) { if (target.datasource !== undefined) { templateizeDatasourceUsage(target); } @@ -88,19 +88,19 @@ export class DashboardExporter { }; // check up panel data sources - for (let panel of saveModel.panels) { + for (const panel of saveModel.panels) { processPanel(panel); // handle collapsed rows if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) { - for (let rowPanel of panel.panels) { + for (const rowPanel of panel.panels) { processPanel(rowPanel); } } } // templatize template vars - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { if (variable.type === 'query') { templateizeDatasourceUsage(variable); variable.options = []; @@ -110,7 +110,7 @@ export class DashboardExporter { } // templatize annotations vars - for (let annotationDef of saveModel.annotations.list) { + for (const annotationDef of saveModel.annotations.list) { templateizeDatasourceUsage(annotationDef); } @@ -129,7 +129,7 @@ export class DashboardExporter { }); // templatize constants - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { if (variable.type === 'constant') { var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); inputs.push({ diff --git a/public/app/features/dashboard/history/history.ts b/public/app/features/dashboard/history/history.ts index be6ad5af1ba..3563ccc7766 100644 --- a/public/app/features/dashboard/history/history.ts +++ b/public/app/features/dashboard/history/history.ts @@ -67,7 +67,7 @@ export class HistoryListCtrl { } revisionSelectionChanged() { - let selected = _.filter(this.revisions, { checked: true }).length; + const selected = _.filter(this.revisions, { checked: true }).length; this.canCompare = selected === 2; } @@ -134,7 +134,7 @@ export class HistoryListCtrl { .getHistoryList(this.dashboard, options) .then(revisions => { // set formatted dates & default values - for (let rev of revisions) { + for (const rev of revisions) { rev.createdDateString = this.formatDate(rev.created); rev.ageString = this.formatBasicDate(rev.created); rev.checked = false; diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 457cac5af72..1d4a70d42aa 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -109,7 +109,7 @@ export class SettingsCtrl { const params = this.$location.search(); const url = this.$location.path(); - for (let section of this.sections) { + for (const section of this.sections) { const sectionParams = _.defaults({ editview: section.id }, params); section.url = config.appSubUrl + url + '?' + $.param(sectionParams); } diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index c32c2a79190..fff307c2510 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -91,7 +91,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, // 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 utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z')); // Older browser does not the internationalization API if (!(window).Intl) { diff --git a/public/app/features/dashboard/specs/dashboard_migration.test.ts b/public/app/features/dashboard/specs/dashboard_migration.test.ts index 07a29d58e65..f440dbb49f2 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.test.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.test.ts @@ -151,18 +151,18 @@ describe('DashboardModel', function() { it('should create proper grid', function() { model.rows = [createRow({ collapse: false, height: 8 }, [[6], [6]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [{ x: 0, y: 0, w: 12, h: 8 }, { x: 12, y: 0, w: 12, h: 8 }]; + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [{ x: 0, y: 0, w: 12, h: 8 }, { x: 12, y: 0, w: 12, h: 8 }]; expect(panelGridPos).toEqual(expectedGrid); }); it('should add special "row" panel if row is collapsed', function() { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, // row { x: 0, y: 1, w: 24, h: 8 }, // row { x: 0, y: 2, w: 24, h: 8 }, @@ -176,9 +176,9 @@ describe('DashboardModel', function() { createRow({ showTitle: true, title: 'Row', height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]]), ]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, // row { x: 0, y: 1, w: 12, h: 8 }, { x: 12, y: 1, w: 12, h: 8 }, @@ -196,9 +196,9 @@ describe('DashboardModel', function() { createRow({ height: 8 }, [[12], [6], [6]]), createRow({ collapse: true, height: 8 }, [[12]]), ]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, // row { x: 0, y: 1, w: 24, h: 8 }, // row { x: 0, y: 2, w: 24, h: 8 }, @@ -214,9 +214,9 @@ describe('DashboardModel', function() { it('should add all rows if even one collapsed or titled row is present', function() { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, // row { x: 0, y: 1, w: 24, h: 8 }, // row { x: 0, y: 2, w: 24, h: 8 }, @@ -230,9 +230,9 @@ describe('DashboardModel', function() { createRow({ height: 6 }, [[6], [6, 3], [6, 3]]), createRow({ height: 6 }, [[4], [4], [4, 3], [4, 3]]), ]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 12, h: 6 }, { x: 12, y: 0, w: 12, h: 3 }, { x: 12, y: 3, w: 12, h: 3 }, @@ -247,9 +247,9 @@ describe('DashboardModel', function() { it('should place panel to the right side of panel having bigger height', function() { model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 8, h: 6 }, { x: 8, y: 0, w: 4, h: 3 }, { x: 12, y: 0, w: 8, h: 6 }, @@ -262,9 +262,9 @@ describe('DashboardModel', function() { it('should fill current row if it possible', function() { model.rows = [createRow({ height: 9 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 8, h: 9 }, { x: 8, y: 0, w: 4, h: 3 }, { x: 12, y: 0, w: 8, h: 6 }, @@ -278,9 +278,9 @@ describe('DashboardModel', function() { it('should fill current row if it possible (2)', function() { model.rows = [createRow({ height: 8 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 8, h: 8 }, { x: 8, y: 0, w: 4, h: 3 }, { x: 12, y: 0, w: 8, h: 6 }, @@ -294,9 +294,9 @@ describe('DashboardModel', function() { it('should fill current row if panel height more than row height', function() { model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 8], [2, 3], [2, 3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 8, h: 6 }, { x: 8, y: 0, w: 4, h: 3 }, { x: 12, y: 0, w: 8, h: 8 }, @@ -309,9 +309,9 @@ describe('DashboardModel', function() { it('should wrap panels to multiple rows', function() { model.rows = [createRow({ height: 6 }, [[6], [6], [12], [6], [3], [3]])]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 12, h: 6 }, { x: 12, y: 0, w: 12, h: 6 }, { x: 0, y: 6, w: 24, h: 6 }, @@ -328,9 +328,9 @@ describe('DashboardModel', function() { createRow({ showTitle: true, title: 'Row', height: 8, repeat: 'server' }, [[6]]), createRow({ height: 8 }, [[12]]), ]; - let dashboard = new DashboardModel(model); - let panelGridPos = getGridPositions(dashboard); - let expectedGrid = [ + const dashboard = new DashboardModel(model); + const panelGridPos = getGridPositions(dashboard); + const expectedGrid = [ { x: 0, y: 0, w: 24, h: 8 }, { x: 0, y: 1, w: 12, h: 8 }, { x: 0, y: 9, w: 24, h: 8 }, @@ -359,7 +359,7 @@ describe('DashboardModel', function() { ), ]; - let dashboard = new DashboardModel(model); + const dashboard = new DashboardModel(model); expect(dashboard.panels[0].repeat).toBe('server'); expect(dashboard.panels.length).toBe(2); }); @@ -368,7 +368,7 @@ describe('DashboardModel', function() { model.rows = [createRow({ height: 8 }, [[6]])]; model.rows[0].panels[0] = { minSpan: 12 }; - let dashboard = new DashboardModel(model); + const dashboard = new DashboardModel(model); expect(dashboard.panels[0].minSpan).toBe(24); }); @@ -376,7 +376,7 @@ describe('DashboardModel', function() { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])]; model.rows[0].panels[0] = {}; - let dashboard = new DashboardModel(model); + const dashboard = new DashboardModel(model); expect(dashboard.panels[0].id).toBe(1); }); }); @@ -386,15 +386,15 @@ function createRow(options, panelDescriptions: any[]) { const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; let { collapse, height, showTitle, title, repeat, repeatIteration } = options; height = height * PANEL_HEIGHT_STEP; - let panels = []; + const panels = []; _.each(panelDescriptions, panelDesc => { - let panel = { span: panelDesc[0] }; + const panel = { span: panelDesc[0] }; if (panelDesc.length > 1) { panel['height'] = panelDesc[1] * PANEL_HEIGHT_STEP; } panels.push(panel); }); - let row = { + const row = { collapse, height, showTitle, diff --git a/public/app/features/dashboard/specs/dashboard_model.test.ts b/public/app/features/dashboard/specs/dashboard_model.test.ts index 6ac642cd58e..28029653a6c 100644 --- a/public/app/features/dashboard/specs/dashboard_model.test.ts +++ b/public/app/features/dashboard/specs/dashboard_model.test.ts @@ -457,16 +457,16 @@ describe('DashboardModel', function() { }); it('getSaveModelClone should return original time when saveTimerange=false', () => { - let options = { saveTimerange: false }; - let saveModel = model.getSaveModelClone(options); + const options = { saveTimerange: false }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.time.from).toBe('now-6h'); expect(saveModel.time.to).toBe('now'); }); it('getSaveModelClone should return updated time when saveTimerange=true', () => { - let options = { saveTimerange: true }; - let saveModel = model.getSaveModelClone(options); + const options = { saveTimerange: true }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.time.from).toBe('now-3h'); expect(saveModel.time.to).toBe('now-1h'); @@ -478,16 +478,16 @@ describe('DashboardModel', function() { }); it('getSaveModelClone should return original time when saveTimerange=false', () => { - let options = { saveTimerange: false }; - let saveModel = model.getSaveModelClone(options); + const options = { saveTimerange: false }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.time.from).toBe('now-6h'); expect(saveModel.time.to).toBe('now'); }); it('getSaveModelClone should return updated time when saveTimerange=true', () => { - let options = { saveTimerange: true }; - let saveModel = model.getSaveModelClone(options); + const options = { saveTimerange: true }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.time.from).toBe('now-3h'); expect(saveModel.time.to).toBe('now-1h'); @@ -542,8 +542,8 @@ describe('DashboardModel', function() { it('getSaveModelClone should return original variable when saveVariables=false', () => { model.templating.list[0].current.text = 'server_002'; - let options = { saveVariables: false }; - let saveModel = model.getSaveModelClone(options); + const options = { saveVariables: false }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.templating.list[0].current.text).toBe('server_001'); }); @@ -551,8 +551,8 @@ describe('DashboardModel', function() { it('getSaveModelClone should return updated variable when saveVariables=true', () => { model.templating.list[0].current.text = 'server_002'; - let options = { saveVariables: true }; - let saveModel = model.getSaveModelClone(options); + const options = { saveVariables: true }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.templating.list[0].current.text).toBe('server_002'); }); @@ -620,8 +620,8 @@ describe('DashboardModel', function() { it('getSaveModelClone should return original variable when saveVariables=false', () => { model.templating.list[0].filters[0].value = 'server 1'; - let options = { saveVariables: false }; - let saveModel = model.getSaveModelClone(options); + const options = { saveVariables: false }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.templating.list[0].filters[0].value).toBe('server 20'); }); @@ -629,8 +629,8 @@ describe('DashboardModel', function() { it('getSaveModelClone should return updated variable when saveVariables=true', () => { model.templating.list[0].filters[0].value = 'server 1'; - let options = { saveVariables: true }; - let saveModel = model.getSaveModelClone(options); + const options = { saveVariables: true }; + const saveModel = model.getSaveModelClone(options); expect(saveModel.templating.list[0].filters[0].value).toBe('server 1'); }); diff --git a/public/app/features/dashboard/specs/history_ctrl.test.ts b/public/app/features/dashboard/specs/history_ctrl.test.ts index 991ecb2c60d..632f3489dae 100644 --- a/public/app/features/dashboard/specs/history_ctrl.test.ts +++ b/public/app/features/dashboard/specs/history_ctrl.test.ts @@ -70,7 +70,7 @@ describe('HistoryListCtrl', () => { }); it('should add a checked property to each revision', () => { - let actual = _.filter(historyListCtrl.revisions, rev => rev.hasOwnProperty('checked')); + const actual = _.filter(historyListCtrl.revisions, rev => rev.hasOwnProperty('checked')); expect(actual.length).toBe(4); }); @@ -78,7 +78,7 @@ describe('HistoryListCtrl', () => { historyListCtrl.revisions[0].checked = true; historyListCtrl.revisions[2].checked = true; historyListCtrl.reset(); - let actual = _.filter(historyListCtrl.revisions, rev => !rev.checked); + const actual = _.filter(historyListCtrl.revisions, rev => !rev.checked); expect(actual.length).toBe(4); }); }); diff --git a/public/app/features/dashboard/specs/history_srv.test.ts b/public/app/features/dashboard/specs/history_srv.test.ts index 401b098a0e1..5c8578ecf39 100644 --- a/public/app/features/dashboard/specs/history_srv.test.ts +++ b/public/app/features/dashboard/specs/history_srv.test.ts @@ -8,7 +8,7 @@ describe('historySrv', function() { const versionsResponse = versions(); const restoreResponse = restore; - let backendSrv = { + const backendSrv = { get: jest.fn(() => Promise.resolve({})), post: jest.fn(() => Promise.resolve({})), }; @@ -44,7 +44,7 @@ describe('historySrv', function() { describe('restoreDashboard', () => { it('should return a success response given valid parameters', function() { - let version = 6; + const version = 6; backendSrv.post = jest.fn(() => Promise.resolve(restoreResponse(version))); historySrv = new HistorySrv(backendSrv); return historySrv.restoreDashboard(dash, version).then(function(response) { @@ -54,7 +54,7 @@ describe('historySrv', function() { it('should return an empty object when not given an id', async () => { historySrv = new HistorySrv(backendSrv); - let rsp = await historySrv.restoreDashboard(emptyDash, 6); + const rsp = await historySrv.restoreDashboard(emptyDash, 6); expect(rsp).toEqual({}); }); }); diff --git a/public/app/features/dashboard/specs/repeat.test.ts b/public/app/features/dashboard/specs/repeat.test.ts index 09bb3b7c494..d8c9e3bc2ed 100644 --- a/public/app/features/dashboard/specs/repeat.test.ts +++ b/public/app/features/dashboard/specs/repeat.test.ts @@ -8,7 +8,7 @@ describe('given dashboard with panel repeat', function() { var dashboard; beforeEach(function() { - let dashboardJSON = { + const dashboardJSON = { panels: [ { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } }, { id: 2, repeat: 'apps', repeatDirection: 'h', gridPos: { x: 0, y: 1, h: 2, w: 8 } }, diff --git a/public/app/features/dashboard/specs/viewstate_srv.test.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts index 08166c6f2bd..740e3c3b9a8 100644 --- a/public/app/features/dashboard/specs/viewstate_srv.test.ts +++ b/public/app/features/dashboard/specs/viewstate_srv.test.ts @@ -4,12 +4,12 @@ import config from 'app/core/config'; import { DashboardViewState } from '../view_state_srv'; describe('when updating view state', () => { - let location = { + const location = { replace: jest.fn(), search: jest.fn(), }; - let $scope = { + const $scope = { onAppEvent: jest.fn(() => {}), dashboard: { meta: {}, @@ -17,7 +17,7 @@ describe('when updating view state', () => { }, }; - let $rootScope = {}; + const $rootScope = {}; let viewState; beforeEach(() => { diff --git a/public/app/features/dashboard/validation_srv.ts b/public/app/features/dashboard/validation_srv.ts index 817be7ca0e3..3e8306039d7 100644 --- a/public/app/features/dashboard/validation_srv.ts +++ b/public/app/features/dashboard/validation_srv.ts @@ -37,7 +37,7 @@ export class ValidationSrv { }); } - let deferred = this.$q.defer(); + const deferred = this.$q.defer(); const promises = []; promises.push(this.backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name })); @@ -54,7 +54,7 @@ export class ValidationSrv { hits = hits.concat(res[1]); } - for (let hit of hits) { + for (const hit of hits) { if (nameLowerCased === hit.title.toLowerCase()) { deferred.reject({ type: 'EXISTING', diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 1ed2d61df71..5bd4db6fddc 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -111,9 +111,9 @@ export class DashboardViewState { } toggleCollapsedPanelRow(panelId) { - for (let panel of this.dashboard.panels) { + for (const panel of this.dashboard.panels) { if (panel.collapsed) { - for (let rowPanel of panel.panels) { + for (const rowPanel of panel.panels) { if (rowPanel.id === panelId) { this.dashboard.toggleRow(panel); return; diff --git a/public/app/features/org/org_users_ctrl.ts b/public/app/features/org/org_users_ctrl.ts index d35b967626a..625e2749399 100644 --- a/public/app/features/org/org_users_ctrl.ts +++ b/public/app/features/org/org_users_ctrl.ts @@ -44,7 +44,7 @@ export class OrgUsersCtrl { } onQueryUpdated() { - let regex = new RegExp(this.searchQuery, 'ig'); + const regex = new RegExp(this.searchQuery, 'ig'); this.users = _.filter(this.unfiltered, item => { return regex.test(item.email) || regex.test(item.login); }); diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 4da40f214a1..94aa142a6b8 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -28,7 +28,7 @@ export class MetricsTabCtrl { this.datasources = datasourceSrv.getMetricSources(); this.panelDsValue = this.panelCtrl.panel.datasource; - for (let ds of this.datasources) { + for (const ds of this.datasources) { if (ds.value === this.panelDsValue) { this.datasourceInstance = ds; } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 6402227164f..6a583b700ef 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -138,7 +138,7 @@ export class PanelCtrl { } getMenu() { - let menu = []; + const menu = []; menu.push({ text: 'View', click: 'ctrl.viewPanel();', @@ -166,7 +166,7 @@ export class PanelCtrl { // Additional items from sub-class menu.push(...this.getAdditionalMenuItems()); - let extendedMenu = this.getExtendedMenu(); + const extendedMenu = this.getExtendedMenu(); menu.push({ text: 'More ...', click: '', @@ -189,7 +189,7 @@ export class PanelCtrl { } getExtendedMenu() { - let menu = []; + const menu = []; if (!this.fullscreen && this.dashboard.meta.canEdit) { menu.push({ text: 'Duplicate', @@ -259,7 +259,7 @@ export class PanelCtrl { } editPanelJson() { - let editScope = this.$scope.$root.$new(); + const editScope = this.$scope.$root.$new(); editScope.object = this.panel.getSaveModel(); editScope.updateHandler = this.replacePanel.bind(this); editScope.enableCopy = true; @@ -276,12 +276,12 @@ export class PanelCtrl { } replacePanel(newPanel, oldPanel) { - let dashboard = this.dashboard; - let index = _.findIndex(dashboard.panels, panel => { + const dashboard = this.dashboard; + const index = _.findIndex(dashboard.panels, panel => { return panel.id === oldPanel.id; }); - let deletedPanel = dashboard.panels.splice(index, 1); + const deletedPanel = dashboard.panels.splice(index, 1); this.dashboard.events.emit('panel-removed', deletedPanel); newPanel = new PanelModel(newPanel); @@ -333,7 +333,7 @@ export class PanelCtrl { if (this.panel.links && this.panel.links.length > 0) { html += ''; @@ -73,7 +73,7 @@ function renderMenuItem(item, ctrl) { function createMenuTemplate(ctrl) { let html = ''; - for (let item of ctrl.getMenu()) { + for (const item of ctrl.getMenu()) { html += renderMenuItem(item, ctrl); } @@ -86,7 +86,7 @@ function panelHeader($compile) { restrict: 'E', template: template, link: function(scope, elem, attrs) { - let menuElem = elem.find('.panel-menu'); + const menuElem = elem.find('.panel-menu'); let menuScope; let isDragged; @@ -99,7 +99,7 @@ function panelHeader($compile) { } menuScope = scope.$new(); - let menuHtml = createMenuTemplate(scope.ctrl); + const menuHtml = createMenuTemplate(scope.ctrl); menuElem.html(menuHtml); $compile(menuElem)(menuScope); @@ -132,12 +132,12 @@ function panelHeader($compile) { .find('[data-toggle=dropdown]') .parentsUntil('.panel') .parent(); - let menuElem = elem.find('[data-toggle=dropdown]').parent(); + const menuElem = elem.find('[data-toggle=dropdown]').parent(); panelElem = panelElem && panelElem.length ? panelElem[0] : undefined; if (panelElem) { panelElem = $(panelElem); $(panelGridClass).removeClass(menuOpenClass); - let state = !menuElem.hasClass('open'); + const state = !menuElem.hasClass('open'); panelElem.toggleClass(menuOpenClass, state); } } diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index 242d2e7da3e..85773a3a778 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -34,7 +34,7 @@ export class SoloPanelCtrl { }; $scope.initPanelScope = function() { - let panelInfo = $scope.dashboard.getPanelInfoById(panelId); + const panelInfo = $scope.dashboard.getPanelInfoById(panelId); // fake row ctrl scope $scope.ctrl = { diff --git a/public/app/features/panellinks/specs/link_srv.test.ts b/public/app/features/panellinks/specs/link_srv.test.ts index 2ec38961e29..521a4edef15 100644 --- a/public/app/features/panellinks/specs/link_srv.test.ts +++ b/public/app/features/panellinks/specs/link_srv.test.ts @@ -2,7 +2,7 @@ import { LinkSrv } from '../link_srv'; import _ from 'lodash'; jest.mock('angular', () => { - let AngularJSMock = require('test/mocks/angular'); + const AngularJSMock = require('test/mocks/angular'); return new AngularJSMock(); }); diff --git a/public/app/features/playlist/playlist_routes.ts b/public/app/features/playlist/playlist_routes.ts index b898820e371..3cb9aceaefb 100644 --- a/public/app/features/playlist/playlist_routes.ts +++ b/public/app/features/playlist/playlist_routes.ts @@ -24,7 +24,7 @@ function grafanaRoutes($routeProvider) { controller: 'PlaylistsCtrl', resolve: { init: function(playlistSrv, $route) { - let playlistId = $route.current.params.id; + const playlistId = $route.current.params.id; playlistSrv.start(playlistId); }, }, diff --git a/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts b/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts index f313c6e8e6a..183947f5072 100644 --- a/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts +++ b/public/app/features/playlist/specs/playlist_edit_ctrl.test.ts @@ -4,7 +4,7 @@ import { PlaylistEditCtrl } from '../playlist_edit_ctrl'; describe('PlaylistEditCtrl', () => { var ctx: any; beforeEach(() => { - let navModelSrv = { + const navModelSrv = { getNav: () => { return { breadcrumbs: [], node: {} }; }, diff --git a/public/app/features/plugins/ds_list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts index 89c760ae253..71c1a516842 100644 --- a/public/app/features/plugins/ds_list_ctrl.ts +++ b/public/app/features/plugins/ds_list_ctrl.ts @@ -17,7 +17,7 @@ export class DataSourcesCtrl { } onQueryUpdated() { - let regex = new RegExp(this.searchQuery, 'ig'); + const regex = new RegExp(this.searchQuery, 'ig'); this.datasources = _.filter(this.unfiltered, item => { regex.lastIndex = 0; return regex.test(item.name) || regex.test(item.type); diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 1936e57f558..bdfb47bc861 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -68,7 +68,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ }, }; - let panelInfo = config.panels[scope.panel.type]; + const panelInfo = config.panels[scope.panel.type]; var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl); if (panelInfo) { panelCtrlPromise = importPluginModule(panelInfo.module).then(function(panelModule) { @@ -107,7 +107,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ switch (attrs.type) { // QueryCtrl case 'query-ctrl': { - let datasource = scope.target.datasource || scope.ctrl.panel.datasource; + const datasource = scope.target.datasource || scope.ctrl.panel.datasource; return datasourceSrv.get(datasource).then(ds => { scope.datasource = ds; @@ -160,7 +160,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ } // AppConfigCtrl case 'app-config-ctrl': { - let model = scope.ctrl.model; + const model = scope.ctrl.model; return importPluginModule(model.module).then(function(appModule) { return { baseUrl: model.baseUrl, @@ -173,7 +173,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ } // App Page case 'app-page': { - let appModel = scope.ctrl.appModel; + const appModel = scope.ctrl.appModel; return importPluginModule(appModel.module).then(function(appModule) { return { baseUrl: appModel.baseUrl, diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 6aa8b2bc38f..93c2008651d 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -53,7 +53,7 @@ export class PluginEditCtrl { url: `plugins/${this.model.id}/edit?tab=config`, }); - let hasDashboards = _.find(model.includes, { type: 'dashboard' }); + const hasDashboards = _.find(model.includes, { type: 'dashboard' }); if (hasDashboards) { this.navModel.main.children.push({ @@ -69,7 +69,7 @@ export class PluginEditCtrl { this.tab = this.$routeParams.tab || defaultTab; - for (let tab of this.navModel.main.children) { + for (const tab of this.navModel.main.children) { if (tab.id === this.tab) { tab.active = true; } @@ -98,7 +98,7 @@ export class PluginEditCtrl { initReadme() { return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => { var md = new Remarkable({ - linkify: true + linkify: true, }); this.readmeHtml = this.$sce.trustAsHtml(md.render(res)); }); diff --git a/public/app/features/plugins/plugin_list_ctrl.ts b/public/app/features/plugins/plugin_list_ctrl.ts index 8e303143946..315252364cc 100644 --- a/public/app/features/plugins/plugin_list_ctrl.ts +++ b/public/app/features/plugins/plugin_list_ctrl.ts @@ -20,7 +20,7 @@ export class PluginListCtrl { } onQueryUpdated() { - let regex = new RegExp(this.searchQuery, 'ig'); + const regex = new RegExp(this.searchQuery, 'ig'); this.plugins = _.filter(this.allPlugins, item => { return regex.test(item.name) || regex.test(item.type); }); diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index cce494d0a60..e227dbb910c 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -140,12 +140,12 @@ const flotDeps = [ 'jquery.flot.events', 'jquery.flot.gauge', ]; -for (let flotDep of flotDeps) { +for (const flotDep of flotDeps) { exposeToPlugin(flotDep, { fakeDep: 1 }); } export function importPluginModule(path: string): Promise { - let builtIn = builtInPlugins[path]; + const builtIn = builtInPlugins[path]; if (builtIn) { return Promise.resolve(builtIn); } diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index 397916aacc8..a2920e55a2a 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/public/app/features/plugins/plugin_page_ctrl.ts @@ -33,7 +33,7 @@ export class AppPageCtrl { return; } - let pluginNav = this.navModelSrv.getNav('plugin-page-' + app.id); + const pluginNav = this.navModelSrv.getNav('plugin-page-' + app.id); this.navModel = { main: { diff --git a/public/app/features/plugins/specs/datasource_srv.test.ts b/public/app/features/plugins/specs/datasource_srv.test.ts index b63e8537837..653e431cb9f 100644 --- a/public/app/features/plugins/specs/datasource_srv.test.ts +++ b/public/app/features/plugins/specs/datasource_srv.test.ts @@ -16,7 +16,7 @@ const templateSrv = { }; describe('datasource_srv', function() { - let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv); + const _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv); describe('when loading explore sources', () => { beforeEach(() => { @@ -46,7 +46,7 @@ describe('datasource_srv', function() { describe('when loading metric sources', () => { let metricSources; - let unsortedDatasources = { + const unsortedDatasources = { mmm: { type: 'test-db', meta: { metrics: { m: 1 } }, diff --git a/public/app/features/templating/specs/editor_ctrl.test.ts b/public/app/features/templating/specs/editor_ctrl.test.ts index f49d0ccd9c6..bba175c2d86 100644 --- a/public/app/features/templating/specs/editor_ctrl.test.ts +++ b/public/app/features/templating/specs/editor_ctrl.test.ts @@ -9,7 +9,7 @@ jest.mock('app/core/app_events', () => { }); describe('VariableEditorCtrl', () => { - let scope = { + const scope = { runQuery: () => { return Promise.resolve({}); }, diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index ea8689f528b..e011d4d0d15 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -5,7 +5,7 @@ import { VariableSrv } from '../variable_srv'; import $q from 'q'; describe('VariableSrv init', function() { - let templateSrv = { + const templateSrv = { init: vars => { this.variables = vars; }, @@ -17,8 +17,8 @@ describe('VariableSrv init', function() { }), }; - let $injector = {}; - let $rootscope = { + const $injector = {}; + const $rootscope = { $on: () => {}, }; diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index bd214639552..e3e75d6a036 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -23,7 +23,7 @@ export class VariableSrv { this.templateSrv.init(this.variables); // init variables - for (let variable of this.variables) { + for (const variable of this.variables) { variable.initLock = this.$q.defer(); } @@ -60,7 +60,7 @@ export class VariableSrv { processVariable(variable, queryParams) { var dependencies = []; - for (let otherVariable of this.variables) { + for (const otherVariable of this.variables) { if (variable.dependsOn(otherVariable)) { dependencies.push(otherVariable.initLock.promise); } @@ -212,13 +212,13 @@ export class VariableSrv { }); let defaultText = urlValue; - let defaultValue = urlValue; + const defaultValue = urlValue; if (!option && _.isArray(urlValue)) { defaultText = []; for (let n = 0; n < urlValue.length; n++) { - let t = _.find(variable.options, op => { + const t = _.find(variable.options, op => { return op.value === urlValue[n]; }); @@ -275,7 +275,7 @@ export class VariableSrv { this.addVariable(variable); } - let filters = variable.filters; + const filters = variable.filters; let filter = _.find(filters, { key: options.key, value: options.value }); if (!filter) { @@ -288,7 +288,7 @@ export class VariableSrv { } createGraph() { - let g = new Graph(); + const g = new Graph(); this.variables.forEach(v1 => { g.createNode(v1.name); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 087bd19da71..63a35e72add 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -45,7 +45,7 @@ export default class CloudWatchDatasource { item.returnData = typeof item.hide === 'undefined' ? true : !item.hide; // valid ExtendedStatistics is like p90.00, check the pattern - let hasInvalidStatistics = item.statistics.some(s => { + const hasInvalidStatistics = item.statistics.some(s => { return s.indexOf('p') === 0 && !/p\d{2}\.\d{2}/.test(s); }); if (hasInvalidStatistics) { @@ -402,7 +402,7 @@ export default class CloudWatchDatasource { value: v, }; }); - let useSelectedVariables = + const useSelectedVariables = selectedVariables.some(s => { return s.value === currentVariables[0].value; }) || currentVariables[0].value === '$__all'; diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index a8968008661..eae3e91d37d 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -4,18 +4,18 @@ import * as dateMath from 'app/core/utils/datemath'; import _ from 'lodash'; describe('CloudWatchDatasource', function() { - let instanceSettings = { + const instanceSettings = { jsonData: { defaultRegion: 'us-east-1', access: 'proxy' }, }; - let templateSrv = { + const templateSrv = { data: {}, templateSettings: { interpolate: /\[\[([\s\S]+?)\]\]/g }, replace: text => _.template(text, templateSrv.templateSettings)(templateSrv.data), variableExists: () => false, }; - let timeSrv = { + const timeSrv = { time: { from: 'now-1h', to: 'now' }, timeRange: () => { return { @@ -24,8 +24,8 @@ describe('CloudWatchDatasource', function() { }; }, }; - let backendSrv = {}; - let ctx = { + const backendSrv = {}; + const ctx = { backendSrv, templateSrv, }; @@ -121,7 +121,7 @@ describe('CloudWatchDatasource', function() { }); }); - it('should cancel query for invalid extended statistics', function () { + it('should cancel query for invalid extended statistics', function() { var query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, @@ -252,7 +252,7 @@ describe('CloudWatchDatasource', function() { function describeMetricFindQuery(query, func) { describe('metricFindQuery ' + query, () => { - let scenario: any = {}; + const scenario: any = {}; scenario.setup = setupCallback => { beforeEach(() => { setupCallback(); @@ -461,12 +461,12 @@ describe('CloudWatchDatasource', function() { 3600, ], ]; - for (let t of testData) { - let target = t[0]; - let options = t[1]; - let now = new Date(options.range.from.valueOf() + t[2] * 1000); - let expected = t[3]; - let actual = ctx.ds.getPeriod(target, options, now); + for (const t of testData) { + const target = t[0]; + const options = t[1]; + const now = new Date(options.range.from.valueOf() + t[2] * 1000); + const expected = t[3]; + const actual = ctx.ds.getPeriod(target, options, now); expect(actual).toBe(expected); } }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 5a8e83a16cb..b77ebe6b738 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -414,13 +414,13 @@ export class ElasticDatasource { return true; } - for (let bucketAgg of target.bucketAggs) { + for (const bucketAgg of target.bucketAggs) { if (this.templateSrv.variableExists(bucketAgg.field) || this.objectContainsTemplate(bucketAgg.settings)) { return true; } } - for (let metric of target.metrics) { + for (const metric of target.metrics) { if ( this.templateSrv.variableExists(metric.field) || this.objectContainsTemplate(metric.settings) || @@ -449,13 +449,13 @@ export class ElasticDatasource { return false; } - for (let key of Object.keys(obj)) { + for (const key of Object.keys(obj)) { if (this.isPrimitive(obj[key])) { if (this.templateSrv.variableExists(obj[key])) { return true; } } else if (Array.isArray(obj[key])) { - for (let item of obj[key]) { + for (const item of obj[key]) { if (this.objectContainsTemplate(item)) { return true; } diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts index a378ab8b55f..e792d290d5b 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.ts +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts @@ -112,29 +112,29 @@ export class ElasticResponse { processAggregationDocs(esAgg, aggDef, target, table, props) { // add columns if (table.columns.length === 0) { - for (let propKey of _.keys(props)) { + for (const propKey of _.keys(props)) { table.addColumn({ text: propKey, filterable: true }); } table.addColumn({ text: aggDef.field, filterable: true }); } // helper func to add values to value array - let addMetricValue = (values, metricName, value) => { + const addMetricValue = (values, metricName, value) => { table.addColumn({ text: metricName }); values.push(value); }; - for (let bucket of esAgg.buckets) { - let values = []; + for (const bucket of esAgg.buckets) { + const values = []; - for (let propValues of _.values(props)) { + for (const propValues of _.values(props)) { values.push(propValues); } // add bucket key (value) values.push(bucket.key); - for (let metric of target.metrics) { + for (const metric of target.metrics) { switch (metric.type) { case 'count': { addMetricValue(values, this.getMetricName(metric.type), bucket.doc_count); @@ -157,7 +157,7 @@ export class ElasticResponse { } default: { let metricName = this.getMetricName(metric.type); - let otherMetrics = _.filter(target.metrics, { type: metric.type }); + const otherMetrics = _.filter(target.metrics, { type: metric.type }); // if more of the same metric type include field field name in property if (otherMetrics.length > 1) { diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts index 36e7a63a005..d1e2e3ba835 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts @@ -6,21 +6,21 @@ import { ElasticDatasource } from '../datasource'; import * as dateMath from 'app/core/utils/datemath'; describe('ElasticDatasource', function() { - let backendSrv = { + const backendSrv = { datasourceRequest: jest.fn(), }; - let $rootScope = { + const $rootScope = { $on: jest.fn(), appEvent: jest.fn(), }; - let templateSrv = { + const templateSrv = { replace: jest.fn(text => text), getAdhocFilters: jest.fn(() => []), }; - let timeSrv = { + const timeSrv = { time: { from: 'now-1h', to: 'now' }, timeRange: jest.fn(() => { return { @@ -33,7 +33,7 @@ describe('ElasticDatasource', function() { }), }; - let ctx = { + const ctx = { $rootScope, backendSrv, }; diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 9fa32fa6503..c3687161414 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -17,7 +17,7 @@ class GrafanaDatasource { if (res.results) { _.forEach(res.results, queryRes => { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index bc1c5722c3f..d4bdabd1f56 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -210,8 +210,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.metricFindQuery = function(query, optionalOptions) { - let options = optionalOptions || {}; - let interpolatedQuery = templateSrv.replace(query); + const options = optionalOptions || {}; + const interpolatedQuery = templateSrv.replace(query); // special handling for tag_values([,]*), this is used for template variables let matches = interpolatedQuery.match(/^tag_values\(([^,]+)((, *[^,]+)*)\)$/); @@ -242,7 +242,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv return this.getTagsAutoComplete(expressions, undefined, options); } - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/metrics/find', params: { @@ -268,9 +268,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getTags = function(optionalOptions) { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/tags', // for cancellations @@ -293,9 +293,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getTagValues = function(tag, optionalOptions) { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/tags/' + templateSrv.replace(tag), // for cancellations @@ -322,9 +322,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getTagsAutoComplete = (expressions, tagPrefix, optionalOptions) => { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/tags/autoComplete/tags', params: { @@ -357,9 +357,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions: any = { + const httpOptions: any = { method: 'GET', url: '/tags/autoComplete/values', params: { @@ -393,9 +393,9 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.getVersion = function(optionalOptions) { - let options = optionalOptions || {}; + const options = optionalOptions || {}; - let httpOptions = { + const httpOptions = { method: 'GET', url: '/version', requestId: options.requestId, @@ -404,7 +404,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv return this.doGraphiteRequest(httpOptions) .then(results => { if (results.data) { - let semver = new SemVersion(results.data); + const semver = new SemVersion(results.data); return semver.isValid() ? results.data : ''; } return ''; @@ -437,7 +437,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv return this.funcDefsPromise; } - let httpOptions = { + const httpOptions = { method: 'GET', url: '/functions', }; @@ -461,7 +461,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv }; this.testDatasource = function() { - let query = { + const query = { panelId: 3, rangeRaw: { from: 'now-1h', to: 'now' }, targets: [{ target: 'constantLine(100)' }], diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index baa58237708..7563a42f583 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -59,11 +59,11 @@ export default class GraphiteQuery { } checkForSeriesByTag() { - let seriesByTagFunc = _.find(this.functions, func => func.def.name === 'seriesByTag'); + const seriesByTagFunc = _.find(this.functions, func => func.def.name === 'seriesByTag'); if (seriesByTagFunc) { this.seriesByTagUsed = true; seriesByTagFunc.hidden = true; - let tags = this.splitSeriesByTagParams(seriesByTagFunc); + const tags = this.splitSeriesByTagParams(seriesByTagFunc); this.tags = tags; } } @@ -186,8 +186,8 @@ export default class GraphiteQuery { let refCount = 0; _.each(targetsByRefId, (t, id) => { if (id !== refId) { - let match = nestedSeriesRefRegex.exec(t.target); - let count = match && match.length ? match.length - 1 : 0; + const match = nestedSeriesRefRegex.exec(t.target); + const count = match && match.length ? match.length - 1 : 0; refCount += count; } }); @@ -232,9 +232,9 @@ export default class GraphiteQuery { const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/; return _.flatten( _.map(func.params, (param: string) => { - let matches = tagPattern.exec(param); + const matches = tagPattern.exec(param); if (matches) { - let tag = matches.slice(1); + const tag = matches.slice(1); if (tag.length === 3) { return { key: tag[0], @@ -253,7 +253,7 @@ export default class GraphiteQuery { } getSeriesByTagFunc() { - let seriesByTagFuncIndex = this.getSeriesByTagFuncIndex(); + const seriesByTagFuncIndex = this.getSeriesByTagFuncIndex(); if (seriesByTagFuncIndex >= 0) { return this.functions[seriesByTagFuncIndex]; } else { @@ -262,7 +262,7 @@ export default class GraphiteQuery { } addTag(tag) { - let newTagParam = renderTagString(tag); + const newTagParam = renderTagString(tag); this.getSeriesByTagFunc().params.push(newTagParam); this.tags.push(tag); } @@ -280,7 +280,7 @@ export default class GraphiteQuery { return; } - let newTagParam = renderTagString(tag); + const newTagParam = renderTagString(tag); this.getSeriesByTagFunc().params[tagIndex] = newTagParam; this.tags[tagIndex] = tag; } diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index 0563de61705..f73c21e4cc7 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -49,7 +49,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { return this.uiSegmentSrv.newSegment(segment); }); - let checkOtherSegmentsIndex = this.queryModel.checkOtherSegmentsIndex || 0; + const checkOtherSegmentsIndex = this.queryModel.checkOtherSegmentsIndex || 0; this.checkOtherSegments(checkOtherSegmentsIndex); if (this.queryModel.seriesByTagUsed) { @@ -195,7 +195,7 @@ export class GraphiteQueryCtrl extends QueryCtrl { } if (segment.type === 'tag') { - let tag = removeTagPrefix(segment.value); + const tag = removeTagPrefix(segment.value); this.pause(); this.addSeriesByTagFunc(tag); return; @@ -273,10 +273,10 @@ export class GraphiteQueryCtrl extends QueryCtrl { } addSeriesByTagFunc(tag) { - let newFunc = this.datasource.createFuncInstance('seriesByTag', { + const newFunc = this.datasource.createFuncInstance('seriesByTag', { withDefaultParams: false, }); - let tagParam = `${tag}=`; + const tagParam = `${tag}=`; newFunc.params = [tagParam]; this.queryModel.addFunction(newFunc); newFunc.added = true; @@ -303,23 +303,23 @@ export class GraphiteQueryCtrl extends QueryCtrl { getAllTags() { return this.datasource.getTags().then(values => { - let altTags = _.map(values, 'text'); + const altTags = _.map(values, 'text'); altTags.splice(0, 0, this.removeTagValue); return mapToDropdownOptions(altTags); }); } getTags(index, tagPrefix) { - let tagExpressions = this.queryModel.renderTagExpressions(index); + const tagExpressions = this.queryModel.renderTagExpressions(index); return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix).then(values => { - let altTags = _.map(values, 'text'); + const altTags = _.map(values, 'text'); altTags.splice(0, 0, this.removeTagValue); return mapToDropdownOptions(altTags); }); } getTagsAsSegments(tagPrefix) { - let tagExpressions = this.queryModel.renderTagExpressions(); + const tagExpressions = this.queryModel.renderTagExpressions(); return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix).then(values => { return _.map(values, val => { return this.uiSegmentSrv.newSegment({ @@ -336,18 +336,18 @@ export class GraphiteQueryCtrl extends QueryCtrl { } getAllTagValues(tag) { - let tagKey = tag.key; + const tagKey = tag.key; return this.datasource.getTagValues(tagKey).then(values => { - let altValues = _.map(values, 'text'); + const altValues = _.map(values, 'text'); return mapToDropdownOptions(altValues); }); } getTagValues(tag, index, valuePrefix) { - let tagExpressions = this.queryModel.renderTagExpressions(index); - let tagKey = tag.key; + const tagExpressions = this.queryModel.renderTagExpressions(index); + const tagKey = tag.key; return this.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix).then(values => { - let altValues = _.map(values, 'text'); + const altValues = _.map(values, 'text'); // Add template variables as additional values _.eachRight(this.templateSrv.variables, variable => { altValues.push('${' + variable.name + ':regex}'); @@ -362,8 +362,8 @@ export class GraphiteQueryCtrl extends QueryCtrl { } addNewTag(segment) { - let newTagKey = segment.value; - let newTag = { key: newTagKey, operator: '=', value: '' }; + const newTagKey = segment.value; + const newTag = { key: newTagKey, operator: '=', value: '' }; this.queryModel.addTag(newTag); this.targetChanged(); this.fixTagSegments(); diff --git a/public/app/plugins/datasource/graphite/specs/datasource.test.ts b/public/app/plugins/datasource/graphite/specs/datasource.test.ts index f94378c57a6..826f2fed344 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource.test.ts @@ -5,7 +5,7 @@ import $q from 'q'; import { TemplateSrvStub } from 'test/specs/helpers'; describe('graphiteDatasource', () => { - let ctx: any = { + const ctx: any = { backendSrv: {}, $q: $q, templateSrv: new TemplateSrvStub(), @@ -18,7 +18,7 @@ describe('graphiteDatasource', () => { }); describe('When querying graphite with one target using query editor target spec', function() { - let query = { + const query = { panelId: 3, dashboardId: 5, rangeRaw: { from: 'now-1h', to: 'now' }, @@ -56,7 +56,7 @@ describe('graphiteDatasource', () => { }); it('should query correctly', function() { - let params = requestOptions.data.split('&'); + const params = requestOptions.data.split('&'); expect(params).toContain('target=prod1.count'); expect(params).toContain('target=prod2.count'); expect(params).toContain('from=-1h'); @@ -64,7 +64,7 @@ describe('graphiteDatasource', () => { }); it('should exclude undefined params', function() { - let params = requestOptions.data.split('&'); + const params = requestOptions.data.split('&'); expect(params).not.toContain('cacheTimeout=undefined'); }); @@ -157,28 +157,28 @@ describe('graphiteDatasource', () => { describe('building graphite params', function() { it('should return empty array if no targets', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{}], }); expect(results.length).toBe(0); }); it('should uri escape targets', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'prod1.{test,test2}' }, { target: 'prod2.count' }], }); expect(results).toContain('target=prod1.%7Btest%2Ctest2%7D'); }); it('should replace target placeholder', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: 'series2' }, { target: 'asPercent(#A,#B)' }], }); expect(results[2]).toBe('target=asPercent(series1%2Cseries2)'); }); it('should replace target placeholder for hidden series', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [ { target: 'series1', hide: true }, { target: 'sumSeries(#A)', hide: true }, @@ -189,28 +189,28 @@ describe('graphiteDatasource', () => { }); it('should replace target placeholder when nesting query references', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: 'sumSeries(#A)' }, { target: 'asPercent(#A,#B)' }], }); expect(results[2]).toBe('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))')); }); it('should fix wrong minute interval parameters', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: "summarize(prod.25m.count, '25m', 'sum')" }], }); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')")); }); it('should fix wrong month interval parameters', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: "summarize(prod.5M.count, '5M', 'sum')" }], }); expect(results[0]).toBe('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')")); }); it('should ignore empty targets', function() { - let results = ctx.ds.buildGraphiteParams({ + const results = ctx.ds.buildGraphiteParams({ targets: [{ target: 'series1' }, { target: '' }], }); expect(results.length).toBe(2); @@ -308,19 +308,19 @@ describe('graphiteDatasource', () => { function accessScenario(name, url, fn) { describe('access scenario ' + name, function() { - let ctx: any = { + const ctx: any = { backendSrv: {}, $q: $q, templateSrv: new TemplateSrvStub(), instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} }, }; - let httpOptions = { + const httpOptions = { headers: {}, }; describe('when using proxy mode', () => { - let options = { dashboardId: 1, panelId: 2 }; + const options = { dashboardId: 1, panelId: 2 }; it('tracing headers should be added', () => { ctx.instanceSettings.url = url; diff --git a/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts b/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts index d54caae05f8..2169db16c25 100644 --- a/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts +++ b/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts @@ -2,7 +2,7 @@ import gfunc from '../gfunc'; import GraphiteQuery from '../graphite_query'; describe('Graphite query model', () => { - let ctx: any = { + const ctx: any = { datasource: { getFuncDef: gfunc.getFuncDef, getFuncDefs: jest.fn().mockReturnValue(Promise.resolve(gfunc.getFuncDefs('1.0'))), diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts index b38ad56427b..7826a458968 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts @@ -3,7 +3,7 @@ import gfunc from '../gfunc'; import { GraphiteQueryCtrl } from '../query_ctrl'; describe('GraphiteQueryCtrl', () => { - let ctx = { + const ctx = { datasource: { metricFindQuery: jest.fn(() => Promise.resolve([])), getFuncDefs: jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0'))), diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index b9f2b2e03fb..8f5850fa0e8 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -147,15 +147,15 @@ export default class InfluxDatasource { } targetContainsTemplate(target) { - for (let group of target.groupBy) { - for (let param of group.params) { + for (const group of target.groupBy) { + for (const param of group.params) { if (this.templateSrv.variableExists(param)) { return true; } } } - for (let i in target.tags) { + for (const i in target.tags) { if (this.templateSrv.variableExists(target.tags[i].value)) { return true; } @@ -219,7 +219,7 @@ export default class InfluxDatasource { return this._seriesQuery(query) .then(res => { - let error = _.get(res, 'results[0].error'); + const error = _.get(res, 'results[0].error'); if (error) { return { status: 'error', message: error }; } @@ -234,7 +234,7 @@ export default class InfluxDatasource { const currentUrl = this.urls.shift(); this.urls.push(currentUrl); - let params: any = {}; + const params: any = {}; if (this.username) { params.u = this.username; @@ -252,7 +252,7 @@ export default class InfluxDatasource { data = null; } - let req: any = { + const req: any = { method: method, url: currentUrl + url, params: params, diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 2ef74170068..1ad684699bf 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -202,10 +202,10 @@ export default class InfluxQuery { var query = 'SELECT '; var i, y; for (i = 0; i < this.selectModels.length; i++) { - let parts = this.selectModels[i]; + const parts = this.selectModels[i]; var selectText = ''; for (y = 0; y < parts.length; y++) { - let part = parts[y]; + const part = parts[y]; selectText = part.render(selectText); } diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index 2be1ecc7bff..1b9cd2962fc 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -36,7 +36,7 @@ export class InfluxQueryCtrl extends QueryCtrl { } this.tagSegments = []; - for (let tag of this.target.tags) { + for (const tag of this.target.tags) { if (!tag.operator) { if (/^\/.*\/$/.test(tag.value)) { tag.operator = '=~'; @@ -106,7 +106,7 @@ export class InfluxQueryCtrl extends QueryCtrl { if (!this.queryModel.hasGroupByTime()) { options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); } - for (let tag of tags) { + for (const tag of tags) { options.push(this.uiSegmentSrv.newSegment({ value: 'tag(' + tag.text + ')' })); } return options; @@ -251,7 +251,7 @@ export class InfluxQueryCtrl extends QueryCtrl { }); if (addTemplateVars) { - for (let variable of this.templateSrv.variables) { + for (const variable of this.templateSrv.variables) { segments.unshift( this.uiSegmentSrv.newSegment({ type: 'value', diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts index 10974cdad97..60f49bd4905 100644 --- a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts @@ -3,7 +3,7 @@ import $q from 'q'; import { TemplateSrvStub } from 'test/specs/helpers'; describe('InfluxDataSource', () => { - let ctx: any = { + const ctx: any = { backendSrv: {}, $q: $q, templateSrv: new TemplateSrvStub(), @@ -16,8 +16,8 @@ describe('InfluxDataSource', () => { }); describe('When issuing metricFindQuery', () => { - let query = 'SELECT max(value) FROM measurement WHERE $timeFilter'; - let queryOptions: any = { + const query = 'SELECT max(value) FROM measurement WHERE $timeFilter'; + const queryOptions: any = { range: { from: '2018-01-01T00:00:00Z', to: '2018-01-02T00:00:00Z', diff --git a/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts b/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts index 4e3fc47a5fd..88d4fb143cd 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_ctrl.test.ts @@ -3,7 +3,7 @@ import { uiSegmentSrv } from 'app/core/services/segment_srv'; import { InfluxQueryCtrl } from '../query_ctrl'; describe('InfluxDBQueryCtrl', () => { - let ctx = {}; + const ctx = {}; beforeEach(() => { InfluxQueryCtrl.prototype.datasource = { diff --git a/public/app/plugins/datasource/mssql/query_ctrl.ts b/public/app/plugins/datasource/mssql/query_ctrl.ts index 884eb634f54..1b64a571c6c 100644 --- a/public/app/plugins/datasource/mssql/query_ctrl.ts +++ b/public/app/plugins/datasource/mssql/query_ctrl.ts @@ -59,7 +59,7 @@ export class MssqlQueryCtrl extends QueryCtrl { this.lastQueryMeta = null; this.lastQueryError = null; - let anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); + const anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); if (anySeriesFromQuery) { this.lastQueryMeta = anySeriesFromQuery.meta; } @@ -67,7 +67,7 @@ export class MssqlQueryCtrl extends QueryCtrl { onDataError(err) { if (err.data && err.data.results) { - let queryRes = err.data.results[this.target.refId]; + const queryRes = err.data.results[this.target.refId]; if (queryRes) { this.lastQueryMeta = queryRes.meta; this.lastQueryError = queryRes.error; diff --git a/public/app/plugins/datasource/mssql/response_parser.ts b/public/app/plugins/datasource/mssql/response_parser.ts index b6f538707b0..0044a49fd7d 100644 --- a/public/app/plugins/datasource/mssql/response_parser.ts +++ b/public/app/plugins/datasource/mssql/response_parser.ts @@ -10,11 +10,11 @@ export default class ResponseParser { return { data: data }; } - for (let key in res.data.results) { - let queryRes = res.data.results[key]; + for (const key in res.data.results) { + const queryRes = res.data.results[key]; if (queryRes.series) { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, @@ -25,7 +25,7 @@ export default class ResponseParser { } if (queryRes.tables) { - for (let table of queryRes.tables) { + for (const table of queryRes.tables) { table.type = 'table'; table.refId = queryRes.refId; table.meta = queryRes.meta; diff --git a/public/app/plugins/datasource/mysql/query_ctrl.ts b/public/app/plugins/datasource/mysql/query_ctrl.ts index 4961ce9e653..1de1fb768ad 100644 --- a/public/app/plugins/datasource/mysql/query_ctrl.ts +++ b/public/app/plugins/datasource/mysql/query_ctrl.ts @@ -57,7 +57,7 @@ export class MysqlQueryCtrl extends QueryCtrl { this.lastQueryMeta = null; this.lastQueryError = null; - let anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); + const anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); if (anySeriesFromQuery) { this.lastQueryMeta = anySeriesFromQuery.meta; } @@ -65,7 +65,7 @@ export class MysqlQueryCtrl extends QueryCtrl { onDataError(err) { if (err.data && err.data.results) { - let queryRes = err.data.results[this.target.refId]; + const queryRes = err.data.results[this.target.refId]; if (queryRes) { this.lastQueryMeta = queryRes.meta; this.lastQueryError = queryRes.error; diff --git a/public/app/plugins/datasource/mysql/response_parser.ts b/public/app/plugins/datasource/mysql/response_parser.ts index e5d8ab79f2a..339dc592ad2 100644 --- a/public/app/plugins/datasource/mysql/response_parser.ts +++ b/public/app/plugins/datasource/mysql/response_parser.ts @@ -10,11 +10,11 @@ export default class ResponseParser { return { data: data }; } - for (let key in res.data.results) { - let queryRes = res.data.results[key]; + for (const key in res.data.results) { + const queryRes = res.data.results[key]; if (queryRes.series) { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, @@ -25,7 +25,7 @@ export default class ResponseParser { } if (queryRes.tables) { - for (let table of queryRes.tables) { + for (const table of queryRes.tables) { table.type = 'table'; table.refId = queryRes.refId; table.meta = queryRes.meta; diff --git a/public/app/plugins/datasource/mysql/specs/datasource.test.ts b/public/app/plugins/datasource/mysql/specs/datasource.test.ts index 85fa2b8cc4e..e75ba5e32ee 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource.test.ts @@ -3,13 +3,13 @@ import { MysqlDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; describe('MySQLDatasource', function() { - let instanceSettings = { name: 'mysql' }; - let backendSrv = {}; - let templateSrv = { + const instanceSettings = { name: 'mysql' }; + const backendSrv = {}; + const templateSrv = { replace: jest.fn(text => text), }; - let ctx = { + const ctx = { backendSrv, }; diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts index 73eca7cffde..befa39fc80e 100644 --- a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts @@ -2,14 +2,14 @@ import OpenTsDatasource from '../datasource'; import $q from 'q'; describe('opentsdb', () => { - let ctx = { + const ctx = { backendSrv: {}, ds: {}, templateSrv: { replace: str => str, }, }; - let instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } }; + const instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } }; beforeEach(() => { ctx.ctrl = new OpenTsDatasource(instanceSettings, $q, ctx.backendSrv, ctx.templateSrv); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 7afd0cf7253..a9073de22cf 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -57,7 +57,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.lastQueryMeta = null; this.lastQueryError = null; - let anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); + const anySeriesFromQuery = _.find(dataList, { refId: this.target.refId }); if (anySeriesFromQuery) { this.lastQueryMeta = anySeriesFromQuery.meta; } @@ -65,7 +65,7 @@ export class PostgresQueryCtrl extends QueryCtrl { onDataError(err) { if (err.data && err.data.results) { - let queryRes = err.data.results[this.target.refId]; + const queryRes = err.data.results[this.target.refId]; if (queryRes) { this.lastQueryMeta = queryRes.meta; this.lastQueryError = queryRes.error; diff --git a/public/app/plugins/datasource/postgres/response_parser.ts b/public/app/plugins/datasource/postgres/response_parser.ts index ebc9598468b..e7f59e13464 100644 --- a/public/app/plugins/datasource/postgres/response_parser.ts +++ b/public/app/plugins/datasource/postgres/response_parser.ts @@ -10,11 +10,11 @@ export default class ResponseParser { return { data: data }; } - for (let key in res.data.results) { - let queryRes = res.data.results[key]; + for (const key in res.data.results) { + const queryRes = res.data.results[key]; if (queryRes.series) { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, @@ -25,7 +25,7 @@ export default class ResponseParser { } if (queryRes.tables) { - for (let table of queryRes.tables) { + for (const table of queryRes.tables) { table.type = 'table'; table.refId = queryRes.refId; table.meta = queryRes.meta; @@ -109,7 +109,7 @@ export default class ResponseParser { const table = data.data.results[options.annotation.name].tables[0]; let timeColumnIndex = -1; - let titleColumnIndex = -1; + const titleColumnIndex = -1; let textColumnIndex = -1; let tagsColumnIndex = -1; diff --git a/public/app/plugins/datasource/postgres/specs/datasource.test.ts b/public/app/plugins/datasource/postgres/specs/datasource.test.ts index cd6f57ee3fc..ea150750687 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource.test.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource.test.ts @@ -3,13 +3,13 @@ import { PostgresDatasource } from '../datasource'; import { CustomVariable } from 'app/features/templating/custom_variable'; describe('PostgreSQLDatasource', function() { - let instanceSettings = { name: 'postgresql' }; + const instanceSettings = { name: 'postgresql' }; - let backendSrv = {}; - let templateSrv = { + const backendSrv = {}; + const templateSrv = { replace: jest.fn(text => text), }; - let ctx = { + const ctx = { backendSrv, }; diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 0a974378cde..396a5fc1cd7 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -24,12 +24,12 @@ export class PromCompleter { } getCompletions(editor, session, pos, prefix, callback) { - let wrappedCallback = (err, completions) => { + const wrappedCallback = (err, completions) => { completions = completions.concat(this.templateVariableCompletions); return callback(err, completions); }; - let token = session.getTokenAt(pos.row, pos.column); + const token = session.getTokenAt(pos.row, pos.column); switch (token.type) { case 'entity.name.tag.label-matcher': @@ -51,8 +51,8 @@ export class PromCompleter { if (token.type === 'paren.lparen' && token.value === '[') { var vectors = []; - for (let unit of ['s', 'm', 'h']) { - for (let value of [1, 5, 10, 30]) { + for (const unit of ['s', 'm', 'h']) { + for (const value of [1, 5, 10, 30]) { vectors.push({ caption: value + unit, value: '[' + value + unit, @@ -99,7 +99,7 @@ export class PromCompleter { } getCompletionsForLabelMatcherName(session, pos) { - let metricName = this.findMetricName(session, pos.row, pos.column); + const metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { return Promise.resolve(this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); } @@ -125,7 +125,7 @@ export class PromCompleter { } getCompletionsForLabelMatcherValue(session, pos) { - let metricName = this.findMetricName(session, pos.row, pos.column); + const metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { return Promise.resolve([]); } @@ -163,7 +163,7 @@ export class PromCompleter { } getCompletionsForBinaryOperator(session, pos) { - let keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier'); + const keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier'); if (!keywordOperatorToken) { return Promise.resolve([]); } @@ -204,7 +204,7 @@ export class PromCompleter { case 'ignoring': case 'group_left': case 'group_right': - let binaryOperatorToken = this.findToken( + const binaryOperatorToken = this.findToken( session, keywordOperatorToken.row, keywordOperatorToken.column, @@ -243,7 +243,7 @@ export class PromCompleter { return labelNames; }); } else { - let metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column); + const metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column); return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => { var labelNames = this.transformToCompletions( _.uniq( @@ -332,7 +332,7 @@ export class PromCompleter { // current row c = 0; for (idx = 0; idx < tokens.length; idx++) { - let nc = c + tokens[idx].value.length; + const nc = c + tokens[idx].value.length; if (nc >= column) { break; } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ec214be8554..057bb55b3c3 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -317,7 +317,7 @@ export class PrometheusDatasource { options = _.clone(options); - for (let target of options.targets) { + for (const target of options.targets) { if (!target.expr || target.hide) { continue; } @@ -482,21 +482,21 @@ export class PrometheusDatasource { return this.$q.when([]); } - let scopedVars = { + const 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); + const 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 sRange = Math.round(msRange / 1000); - let regularRange = kbn.secondsToHms(msRange / 1000); + const range = this.timeSrv.timeRange(); + const msRange = range.to.diff(range.from); + const sRange = Math.round(msRange / 1000); + const regularRange = kbn.secondsToHms(msRange / 1000); return { __range_ms: { text: msRange, value: msRange }, __range_s: { text: sRange, value: sRange }, @@ -537,7 +537,7 @@ export class PrometheusDatasource { }) .value(); - for (let value of series.values) { + for (const value of series.values) { if (value[1] === '1') { var event = { annotation: annotation, @@ -557,7 +557,7 @@ export class PrometheusDatasource { } testDatasource() { - let now = new Date().getTime(); + const now = new Date().getTime(); return this.performInstantQuery({ expr: '1+1' }, now / 1000).then(response => { if (response.data.status === 'success') { return { status: 'success', message: 'Data source is working' }; diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 7cb160e2d8c..1b1420c0b46 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -5,21 +5,21 @@ export class ResultTransformer { constructor(private templateSrv) {} transform(response: any, options: any): any[] { - let prometheusResult = response.data.data.result; + const prometheusResult = response.data.data.result; if (options.format === 'table') { return [this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)]; } else if (options.format === 'heatmap') { let seriesList = []; prometheusResult.sort(sortSeriesByLabel); - for (let metricData of prometheusResult) { + for (const metricData of prometheusResult) { seriesList.push(this.transformMetricData(metricData, options, options.start, options.end)); } seriesList = this.transformToHistogramOverTime(seriesList); return seriesList; } else { - let seriesList = []; - for (let metricData of prometheusResult) { + const seriesList = []; + for (const metricData of prometheusResult) { if (response.data.data.resultType === 'matrix') { seriesList.push(this.transformMetricData(metricData, options, options.start, options.end)); } else if (response.data.data.resultType === 'vector') { @@ -44,7 +44,7 @@ export class ResultTransformer { throw new Error('Prometheus heatmap error: data should be a time series'); } - for (let value of metricData.values) { + for (const value of metricData.values) { let dp_value = parseFloat(value[1]); if (_.isNaN(dp_value)) { dp_value = null; @@ -96,7 +96,7 @@ export class ResultTransformer { metricLabels[label] = labelIndex + 1; table.columns.push({ text: label, filterable: !label.startsWith('__') }); }); - let valueText = resultCount > 1 ? `Value #${refId}` : 'Value'; + const valueText = resultCount > 1 ? `Value #${refId}` : 'Value'; table.columns.push({ text: valueText }); // Populate rows, set value to empty string when label not present. @@ -175,8 +175,8 @@ export class ResultTransformer { le30 30 10 35 => 10 0 5 */ for (let i = seriesList.length - 1; i > 0; i--) { - let topSeries = seriesList[i].datapoints; - let bottomSeries = seriesList[i - 1].datapoints; + const topSeries = seriesList[i].datapoints; + const bottomSeries = seriesList[i - 1].datapoints; if (!topSeries || !bottomSeries) { throw new Error('Prometheus heatmap transform error: data should be a time series'); } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index b29e4d27233..59fcc6592fb 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -13,10 +13,10 @@ describe('Prometheus editor completer', function() { }; } - let editor = {}; + const editor = {}; - let backendSrv = {}; - let datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); + const backendSrv = {}; + const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); datasourceStub.performInstantQuery = jest.fn(() => Promise.resolve({ @@ -36,7 +36,7 @@ describe('Prometheus editor completer', function() { ); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); - let templateSrv = { + const templateSrv = { variables: [ { name: 'var_name', @@ -44,7 +44,7 @@ describe('Prometheus editor completer', function() { }, ], }; - let completer = new PromCompleter(datasourceStub, templateSrv); + const completer = new PromCompleter(datasourceStub, templateSrv); describe('When inside brackets', () => { it('Should return range vectors', () => { diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index d52019ac4cc..fd963f7986e 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -14,8 +14,8 @@ import { jest.mock('../metric_find_query'); describe('PrometheusDatasource', () => { - let ctx: any = {}; - let instanceSettings = { + const ctx: any = {}; + const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', @@ -123,7 +123,7 @@ describe('PrometheusDatasource', () => { ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock); return ctx.ds.query(ctx.query).then(result => { - let results = result.data; + const results = result.data; return expect(results).toMatchObject(expected); }); }); @@ -153,7 +153,7 @@ describe('PrometheusDatasource', () => { ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock); return ctx.ds.query(ctx.query).then(result => { - let seriesLabels = _.map(result.data, 'target'); + const seriesLabels = _.map(result.data, 'target'); return expect(seriesLabels).toEqual(expected); }); }); @@ -326,7 +326,7 @@ describe('PrometheusDatasource', () => { describe('metricFindQuery', () => { beforeEach(() => { - let query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))'; + const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))'; ctx.templateSrvMock.replace = jest.fn(); ctx.timeSrvMock.timeRange = () => { return { @@ -343,17 +343,17 @@ 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; + const range = ctx.templateSrvMock.replace.mock.calls[0][1].__range; + const rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms; + const 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', () => { - let interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval; - let intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms; + const interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval; + const intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms; expect(interval).toEqual({ text: '15s', value: '15s' }); expect(intervalMs).toEqual({ text: 15000, value: 15000 }); }); @@ -385,23 +385,23 @@ const HOUR = 60 * MINUTE; const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND); -let ctx = {}; -let instanceSettings = { +const ctx = {}; +const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp', jsonData: { httpMethod: 'GET' }, }; -let backendSrv = { +const backendSrv = { datasourceRequest: jest.fn(), }; -let templateSrv = { +const templateSrv = { replace: jest.fn(str => str), }; -let timeSrv = { +const timeSrv = { timeRange: () => { return { to: { diff: () => 2000 }, from: '' }; }, @@ -420,7 +420,7 @@ describe('PrometheusDatasource', () => { 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; beforeEach(async () => { - let response = { + const response = { data: { status: 'success', data: { @@ -443,7 +443,7 @@ describe('PrometheusDatasource', () => { }); it('should generate the correct query', () => { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -465,7 +465,7 @@ describe('PrometheusDatasource', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -530,7 +530,7 @@ describe('PrometheusDatasource', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -553,7 +553,7 @@ describe('PrometheusDatasource', () => { }); }); it('should generate the correct query', () => { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -579,7 +579,7 @@ describe('PrometheusDatasource', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -625,7 +625,7 @@ describe('PrometheusDatasource', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -664,7 +664,7 @@ describe('PrometheusDatasource', () => { }; it('should be min interval when greater than auto interval', async () => { - let query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -675,12 +675,12 @@ describe('PrometheusDatasource', () => { ], interval: '5s', }; - let urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -696,7 +696,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -717,7 +717,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -734,7 +734,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -756,7 +756,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -777,7 +777,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -799,7 +799,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -821,7 +821,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -843,7 +843,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); }); @@ -886,7 +886,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -925,7 +925,7 @@ describe('PrometheusDatasource', () => { templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -965,7 +965,7 @@ describe('PrometheusDatasource', () => { templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1011,7 +1011,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1051,7 +1051,7 @@ describe('PrometheusDatasource', () => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1096,7 +1096,7 @@ describe('PrometheusDatasource', () => { templateSrv.replace = jest.fn(str => str); ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); expect(res.url).toBe(urlExpected); @@ -1116,7 +1116,7 @@ describe('PrometheusDatasource', () => { describe('PrometheusDatasource for POST', () => { // var ctx = new helpers.ServiceTestContext(); - let instanceSettings = { + const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', @@ -1140,7 +1140,7 @@ describe('PrometheusDatasource for POST', () => { }; beforeEach(async () => { - let response = { + const response = { status: 'success', data: { data: { @@ -1161,7 +1161,7 @@ describe('PrometheusDatasource for POST', () => { }); }); it('should generate the correct query', () => { - let res = backendSrv.datasourceRequest.mock.calls[0][0]; + const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('POST'); expect(res.url).toBe(urlExpected); expect(res.data).toEqual(dataExpected); diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts index 88f6830cd31..bfbf241ba06 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts @@ -4,7 +4,7 @@ import PrometheusMetricFindQuery from '../metric_find_query'; import q from 'q'; describe('PrometheusMetricFindQuery', function() { - let instanceSettings = { + const instanceSettings = { url: 'proxied', directUrl: 'direct', user: 'test', @@ -15,7 +15,7 @@ describe('PrometheusMetricFindQuery', function() { from: moment.utc('2018-04-25 10:00'), to: moment.utc('2018-04-25 11:00'), }; - let ctx: any = { + const ctx: any = { backendSrvMock: { datasourceRequest: jest.fn(() => Promise.resolve({})), }, diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts index 68224121414..ac85e1374bb 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts @@ -1,7 +1,7 @@ import { ResultTransformer } from '../result_transformer'; describe('Prometheus Result Transformer', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.templateSrv = { @@ -111,7 +111,7 @@ describe('Prometheus Result Transformer', () => { }; it('should convert cumulative histogram to regular', () => { - let options = { + const options = { format: 'heatmap', start: 1445000010, end: 1445000030, @@ -171,7 +171,7 @@ describe('Prometheus Result Transformer', () => { ], }, }; - let options = { + const options = { format: 'timeseries', start: 0, end: 2, @@ -194,7 +194,7 @@ describe('Prometheus Result Transformer', () => { ], }, }; - let options = { + const options = { format: 'timeseries', step: 1, start: 0, @@ -218,7 +218,7 @@ describe('Prometheus Result Transformer', () => { ], }, }; - let options = { + const options = { format: 'timeseries', step: 2, start: 0, diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index 327abb1d70b..3f4035830ed 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -39,7 +39,7 @@ class TestDataDatasource { if (res.results) { _.forEach(res.results, queryRes => { - for (let series of queryRes.series) { + for (const series of queryRes.series) { data.push({ target: series.name, datapoints: series.points, diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index 55869ce626d..b171f590e94 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -42,7 +42,7 @@ class AlertListPanel extends PanelCtrl { this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.events.on('refresh', this.onRefresh.bind(this)); - for (let key in this.panel.stateFilter) { + for (const key in this.panel.stateFilter) { this.stateFilter[this.panel.stateFilter[key]] = true; } } @@ -67,7 +67,7 @@ class AlertListPanel extends PanelCtrl { updateStateFilter() { var result = []; - for (let key in this.stateFilter) { + for (const key in this.stateFilter) { if (this.stateFilter[key]) { result.push(key); } diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index f8162c57a10..0e75399445d 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -14,7 +14,7 @@ export class DataProcessor { var firstItem; if (options.dataList && options.dataList.length > 0) { firstItem = options.dataList[0]; - let autoDetectMode = this.getAutoDetectXAxisMode(firstItem); + const autoDetectMode = this.getAutoDetectXAxisMode(firstItem); if (this.panel.xaxis.mode !== autoDetectMode) { this.panel.xaxis.mode = autoDetectMode; this.setPanelDefaultsForNewXAxisMode(); @@ -127,7 +127,7 @@ export class DataProcessor { } customHandler(dataItem) { - let nameField = this.panel.xaxis.name; + const nameField = this.panel.xaxis.name; if (!nameField) { throw { message: 'No field name specified to use for x-axis, check your axes settings', @@ -159,9 +159,9 @@ export class DataProcessor { return []; } - let fields = []; + const fields = []; var firstItem = dataList[0]; - let fieldParts = []; + const fieldParts = []; function getPropertiesRecursive(obj) { _.forEach(obj, (value, key) => { @@ -170,7 +170,7 @@ export class DataProcessor { getPropertiesRecursive(value); } else { if (!onlyNumbers || _.isNumber(value)) { - let field = fieldParts.concat(key).join('.'); + const field = fieldParts.concat(key).join('.'); fields.push(field); } } @@ -205,7 +205,7 @@ export class DataProcessor { } pluckDeep(obj: any, property: string) { - let propertyParts = property.split('.'); + const propertyParts = property.split('.'); let value = obj; for (let i = 0; i < propertyParts.length; ++i) { if (value[propertyParts[i]]) { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 35886aa5bf7..bfbbc855f20 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -148,7 +148,7 @@ class GraphElement { 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; + const isRangeSelection = pos.x !== pos.x1; if (!isRangeSelection) { setTimeout(() => { this.eventManager.updateTime({ from: pos.x, to: null }); @@ -269,7 +269,7 @@ class GraphElement { this.panel.dashes = this.panel.lines ? this.panel.dashes : false; // Populate element - let options: any = this.buildFlotOptions(this.panel); + const options: any = this.buildFlotOptions(this.panel); this.prepareXAxis(options, this.panel); this.configureYAxisOptions(this.data, options); this.thresholdManager.addFlotOptions(options, this.panel); @@ -281,7 +281,7 @@ class GraphElement { buildFlotPairs(data) { for (let i = 0; i < data.length; i++) { - let series = data[i]; + const series = data[i]; series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode); // if hidden remove points and disable stack @@ -299,7 +299,7 @@ class GraphElement { options.series.bars.align = 'center'; for (let i = 0; i < this.data.length; i++) { - let series = this.data[i]; + const series = this.data[i]; series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]]; } @@ -310,9 +310,9 @@ class GraphElement { 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; + const histMin = _.min(_.map(this.data, s => s.stats.min)); + const histMax = _.max(_.map(this.data, s => s.stats.max)); + const 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); @@ -362,7 +362,7 @@ class GraphElement { gridColor = '#a1a1a1'; } const stack = panel.stack ? true : null; - let options = { + const options = { hooks: { draw: [this.drawHook.bind(this)], processOffset: [this.processOffsetHook.bind(this)], @@ -481,12 +481,12 @@ class GraphElement { addXHistogramAxis(options, bucketSize) { let ticks, min, max; - let defaultTicks = this.panelWidth / 50; + const defaultTicks = this.panelWidth / 50; if (this.data.length && bucketSize) { - let tick_values = []; - for (let d of this.data) { - for (let point of d.data) { + const tick_values = []; + for (const d of this.data) { + for (const point of d.data) { tick_values[point[0]] = true; } } diff --git a/public/app/plugins/panel/graph/graph_tooltip.ts b/public/app/plugins/panel/graph/graph_tooltip.ts index 7bbafc453eb..da2d25b1366 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.ts +++ b/public/app/plugins/panel/graph/graph_tooltip.ts @@ -2,20 +2,20 @@ import $ from 'jquery'; import { appEvents } from 'app/core/core'; export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { - let self = this; - let ctrl = scope.ctrl; - let panel = ctrl.panel; + const self = this; + const ctrl = scope.ctrl; + const panel = ctrl.panel; - let $tooltip = $('
    '); + const $tooltip = $('
    '); this.destroy = function() { $tooltip.remove(); }; this.findHoverIndexFromDataPoints = function(posX, series, last) { - let ps = series.datapoints.pointsize; - let initial = last * ps; - let len = series.datapoints.points.length; + const ps = series.datapoints.pointsize; + const initial = last * ps; + const len = series.datapoints.points.length; let j; for (j = initial; j < len; j += ps) { // Special case of a non stepped line, highlight the very last point just before a null point @@ -149,7 +149,7 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { elem.mouseleave(function() { if (panel.tooltip.shared) { - let plot = elem.data().plot; + const plot = elem.data().plot; if (plot) { $tooltip.detach(); plot.unhighlight(); @@ -177,25 +177,25 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { }; this.show = function(pos, item) { - let plot = elem.data().plot; - let plotData = plot.getData(); - let xAxes = plot.getXAxes(); - let xMode = xAxes[0].options.mode; - let seriesList = getSeriesFn(); + const plot = elem.data().plot; + const plotData = plot.getData(); + const xAxes = plot.getXAxes(); + const xMode = xAxes[0].options.mode; + const seriesList = getSeriesFn(); let allSeriesMode = panel.tooltip.shared; let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; // if panelRelY is defined another panel wants us to show a tooltip // get pageX from position on x axis and pageY from relative position in original panel if (pos.panelRelY) { - let pointOffset = plot.pointOffset({ x: pos.x }); + const pointOffset = plot.pointOffset({ x: pos.x }); if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) { self.clear(plot); return; } pos.pageX = elem.offset().left + pointOffset.left; pos.pageY = elem.offset().top + elem.height() * pos.panelRelY; - let isVisible = + const isVisible = pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop(); if (!isVisible) { self.clear(plot); @@ -223,7 +223,7 @@ export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) { if (allSeriesMode) { plot.unhighlight(); - let seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos); + const seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos); seriesHtml = ''; diff --git a/public/app/plugins/panel/graph/histogram.ts b/public/app/plugins/panel/graph/histogram.ts index ad56e477a85..f8819041cba 100644 --- a/public/app/plugins/panel/graph/histogram.ts +++ b/public/app/plugins/panel/graph/histogram.ts @@ -7,12 +7,12 @@ import TimeSeries from 'app/core/time_series2'; */ export function getSeriesValues(dataList: TimeSeries[]): number[] { const VALUE_INDEX = 0; - let values = []; + const values = []; // Count histogam stats for (let i = 0; i < dataList.length; i++) { - let series = dataList[i]; - let datapoints = series.datapoints; + const series = dataList[i]; + const datapoints = series.datapoints; for (let j = 0; j < datapoints.length; j++) { if (datapoints[j][VALUE_INDEX] !== null) { values.push(datapoints[j][VALUE_INDEX]); @@ -30,10 +30,10 @@ export function getSeriesValues(dataList: TimeSeries[]): number[] { * @param bucketSize */ export function convertValuesToHistogram(values: number[], bucketSize: number, min: number, max: number): any[] { - let histogram = {}; + const histogram = {}; - let minBound = getBucketBound(min, bucketSize); - let maxBound = getBucketBound(max, bucketSize); + const minBound = getBucketBound(min, bucketSize); + const maxBound = getBucketBound(max, bucketSize); let bound = minBound; let n = 0; while (bound <= maxBound) { @@ -43,11 +43,11 @@ export function convertValuesToHistogram(values: number[], bucketSize: number, m } for (let i = 0; i < values.length; i++) { - let bound = getBucketBound(values[i], bucketSize); + const bound = getBucketBound(values[i], bucketSize); histogram[bound] = histogram[bound] + 1; } - let histogam_series = _.map(histogram, (count, bound) => { + const histogam_series = _.map(histogram, (count, bound) => { return [Number(bound), count]; }); @@ -68,10 +68,10 @@ export function convertToHistogramData( max: number ): any[] { return data.map(series => { - let values = getSeriesValues([series]); + const values = getSeriesValues([series]); series.histogram = true; if (!hiddenSeries[series.alias]) { - let histogram = convertValuesToHistogram(values, bucketSize, min, max); + const histogram = convertValuesToHistogram(values, bucketSize, min, max); series.data = histogram; } else { series.data = []; diff --git a/public/app/plugins/panel/graph/jquery.flot.events.ts b/public/app/plugins/panel/graph/jquery.flot.events.ts index 9dfe0a8573f..2f05a76d02b 100644 --- a/public/app/plugins/panel/graph/jquery.flot.events.ts +++ b/public/app/plugins/panel/graph/jquery.flot.events.ts @@ -5,16 +5,16 @@ import Drop from 'tether-drop'; /** @ngInject */ export function createAnnotationToolip(element, event, plot) { - let injector = angular.element(document).injector(); - let content = document.createElement('div'); + const injector = angular.element(document).injector(); + const content = document.createElement('div'); content.innerHTML = ''; injector.invoke([ '$compile', '$rootScope', function($compile, $rootScope) { - let eventManager = plot.getOptions().events.manager; - let tmpScope = $rootScope.$new(true); + const eventManager = plot.getOptions().events.manager; + const tmpScope = $rootScope.$new(true); tmpScope.event = event; tmpScope.onEdit = function() { eventManager.editEvent(event); @@ -24,7 +24,7 @@ export function createAnnotationToolip(element, event, plot) { tmpScope.$digest(); tmpScope.$destroy(); - let drop = new Drop({ + const drop = new Drop({ target: element[0], content: content, position: 'bottom center', @@ -51,7 +51,7 @@ let markerElementToAttachTo = null; /** @ngInject */ export function createEditPopover(element, event, plot) { - let eventManager = plot.getOptions().events.manager; + const 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) @@ -66,15 +66,15 @@ export function createEditPopover(element, event, plot) { // wait for element to be attached and positioned setTimeout(function() { - let injector = angular.element(document).injector(); - let content = document.createElement('div'); + const injector = angular.element(document).injector(); + const content = document.createElement('div'); content.innerHTML = ''; injector.invoke([ '$compile', '$rootScope', function($compile, $rootScope) { - let scope = $rootScope.$new(true); + const scope = $rootScope.$new(true); let drop; scope.event = event; @@ -240,22 +240,22 @@ export class EventMarkers { * create internal objects for the given events */ setupEvents(events) { - let parts = _.partition(events, 'isRegion'); - let regions = parts[0]; + const parts = _.partition(events, 'isRegion'); + const regions = parts[0]; events = parts[1]; $.each(events, (index, event) => { - let ve = new VisualEvent(event, this._buildDiv(event)); + const ve = new VisualEvent(event, this._buildDiv(event)); this._events.push(ve); }); $.each(regions, (index, event) => { - let vre = new VisualEvent(event, this._buildRegDiv(event)); + const vre = new VisualEvent(event, this._buildRegDiv(event)); this._events.push(vre); }); this._events.sort((a, b) => { - let ao = a.getOptions(), + const ao = a.getOptions(), bo = b.getOptions(); if (ao.min > bo.min) { return 1; @@ -293,7 +293,7 @@ export class EventMarkers { let o = this._plot.getPlotOffset(), left, top; - let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; $.each(this._events, (index, event) => { top = o.top + this._plot.height() - event.visual().height(); @@ -316,16 +316,16 @@ export class EventMarkers { * create a DOM element for the given event */ _buildDiv(event) { - let that = this; + const that = this; - let container = this._plot.getPlaceholder(); - let o = this._plot.getPlotOffset(); - let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + const container = this._plot.getPlaceholder(); + const o = this._plot.getPlotOffset(); + const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; let top, left, color, markerSize, markerShow, lineStyle, lineWidth; let markerTooltip; // map the eventType to a types object - let eventTypeId = event.eventType; + const eventTypeId = event.eventType; if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) { color = '#666'; @@ -369,7 +369,7 @@ export class EventMarkers { top = o.top + this._plot.height() + topOffset; left = xaxis.p2c(event.min) + o.left; - let line = $('
    ') + const line = $('
    ') .css({ position: 'absolute', opacity: 0.8, @@ -385,7 +385,7 @@ export class EventMarkers { .appendTo(container); if (markerShow) { - let marker = $('
    ').css({ + const marker = $('
    ').css({ position: 'absolute', left: -markerSize - Math.round(lineWidth / 2) + 'px', 'font-size': 0, @@ -420,7 +420,7 @@ export class EventMarkers { event: event, }); - let mouseenter = function() { + const mouseenter = function() { createAnnotationToolip(marker, $(this).data('event'), that._plot); }; @@ -428,7 +428,7 @@ export class EventMarkers { createEditPopover(marker, event.editModel, that._plot); } - let mouseleave = function() { + const mouseleave = function() { that._plot.clearSelection(); }; @@ -438,7 +438,7 @@ export class EventMarkers { } } - let drawableEvent = new DrawableEvent( + const drawableEvent = new DrawableEvent( line, function drawFunc(obj) { obj.show(); @@ -465,15 +465,15 @@ export class EventMarkers { * create a DOM element for the given region */ _buildRegDiv(event) { - let that = this; + const that = this; - let container = this._plot.getPlaceholder(); - let o = this._plot.getPlotOffset(); - let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + const container = this._plot.getPlaceholder(); + const o = this._plot.getPlotOffset(); + const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; let top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip; // map the eventType to a types object - let eventTypeId = event.eventType; + const eventTypeId = event.eventType; if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) { color = '#666'; @@ -499,17 +499,17 @@ export class EventMarkers { lineStyle = this._types[eventTypeId].lineStyle.toLowerCase(); } - let topOffset = 2; + const 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); + const timeFrom = Math.min(event.min, event.timeEnd); + const timeTo = Math.max(event.min, event.timeEnd); left = xaxis.p2c(timeFrom) + o.left; - let right = xaxis.p2c(timeTo) + o.left; + const right = xaxis.p2c(timeTo) + o.left; regionWidth = right - left; _.each([left, right], position => { - let line = $('
    ').css({ + const line = $('
    ').css({ position: 'absolute', opacity: 0.8, left: position + 'px', @@ -524,7 +524,7 @@ export class EventMarkers { line.appendTo(container); }); - let region = $('
    ').css({ + const region = $('
    ').css({ position: 'absolute', opacity: 0.5, left: left + 'px', @@ -541,7 +541,7 @@ export class EventMarkers { event: event, }); - let mouseenter = function() { + const mouseenter = function() { createAnnotationToolip(region, $(this).data('event'), that._plot); }; @@ -549,7 +549,7 @@ export class EventMarkers { createEditPopover(region, event.editModel, that._plot); } - let mouseleave = function() { + const mouseleave = function() { that._plot.clearSelection(); }; @@ -558,7 +558,7 @@ export class EventMarkers { region.hover(mouseenter, mouseleave); } - let drawableEvent = new DrawableEvent( + const drawableEvent = new DrawableEvent( region, function drawFunc(obj) { obj.show(); @@ -585,8 +585,8 @@ export class EventMarkers { * check if the event is inside visible range */ _insidePlot(x) { - let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; - let xc = xaxis.p2c(x); + const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1]; + const xc = xaxis.p2c(x); return xc > 0 && xc < xaxis.p2c(xaxis.max); } } @@ -598,8 +598,8 @@ export class EventMarkers { /** @ngInject */ export function init(plot) { /*jshint validthis:true */ - let that = this; - let eventMarkers = new EventMarkers(plot); + const that = this; + const eventMarkers = new EventMarkers(plot); plot.getEvents = function() { return eventMarkers._events; @@ -638,7 +638,7 @@ export function init(plot) { }); plot.hooks.draw.push(function(plot) { - let options = plot.getOptions(); + const options = plot.getOptions(); if (eventMarkers.eventsEnabled) { // check for first run @@ -654,7 +654,7 @@ export function init(plot) { }); } -let defaultOptions = { +const defaultOptions = { events: { data: null, types: null, diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index f5c35ad98bf..f735fe28b22 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -16,7 +16,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { var i; var legendScrollbar; const legendRightDefaultWidth = 10; - let legendElem = elem.parent(); + const legendElem = elem.parent(); scope.$on('$destroy', function() { destroyScrollbar(); @@ -111,7 +111,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function render() { - let legendWidth = legendElem.width(); + const legendWidth = legendElem.width(); if (!ctrl.panel.legend.show) { elem.empty(); firstRender = true; @@ -176,7 +176,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function renderSeriesLegendElements() { - let seriesElements = []; + const seriesElements = []; for (i = 0; i < seriesList.length; i++) { var series = seriesList[i]; @@ -231,7 +231,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function renderLegendElement(tableHeaderElem) { - let legendWidth = elem.width(); + const legendWidth = elem.width(); var seriesElements = renderSeriesLegendElements(); @@ -262,8 +262,8 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
    `; - let scrollRoot = elem; - let scroller = elem.find('.graph-legend-scroll'); + const scrollRoot = elem; + const scroller = elem.find('.graph-legend-scroll'); // clear existing scroll bar track to prevent duplication scrollRoot.find('.baron__track').remove(); @@ -272,7 +272,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { $(scrollBarHTML).appendTo(scrollRoot); scroller.addClass(scrollerClass); - let scrollbarParams = { + const scrollbarParams = { root: scrollRoot[0], scroller: scroller[0], bar: '.baron__bar', diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index ba151692147..97999158446 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -196,7 +196,7 @@ class GraphCtrl extends MetricsPanelCtrl { tip: 'No datapoints returned from data query', }; } else { - for (let series of this.seriesList) { + for (const series of this.seriesList) { if (series.isOutsideRange) { this.dataWarning = { title: 'Data points outside time range', @@ -226,7 +226,7 @@ class GraphCtrl extends MetricsPanelCtrl { return; } - for (let series of this.seriesList) { + for (const series of this.seriesList) { series.applySeriesOverrides(this.panel.seriesOverrides); if (series.unit) { diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index f75f7cd68ea..2ae76bb9c9c 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -28,9 +28,9 @@ import moment from 'moment'; import $ from 'jquery'; import { graphDirective } from '../graph'; -let ctx = {}; +const ctx = {}; let ctrl; -let scope = { +const scope = { ctrl: {}, range: { from: moment([2015, 1, 1]), diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts index a0c7dd0ab9c..49efa8d4120 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts @@ -4,7 +4,7 @@ import { GraphCtrl } from '../module'; jest.mock('../graph', () => ({})); describe('GraphCtrl', () => { - let injector = { + const injector = { get: () => { return { timeRange: () => { @@ -17,7 +17,7 @@ describe('GraphCtrl', () => { }, }; - let scope = { + const scope = { $on: () => {}, }; @@ -30,7 +30,7 @@ describe('GraphCtrl', () => { }, }; - let ctx = {}; + const ctx = {}; beforeEach(() => { ctx.ctrl = new GraphCtrl(scope, injector, {}); diff --git a/public/app/plugins/panel/graph/specs/histogram.test.ts b/public/app/plugins/panel/graph/specs/histogram.test.ts index 0e9eaa8b98e..adbc0fcba68 100644 --- a/public/app/plugins/panel/graph/specs/histogram.test.ts +++ b/public/app/plugins/panel/graph/specs/histogram.test.ts @@ -11,17 +11,17 @@ describe('Graph Histogam Converter', function() { it('Should convert to series-like array', () => { bucketSize = 10; - let expected = [[0, 2], [10, 3], [20, 2]]; + const expected = [[0, 2], [10, 3], [20, 2]]; - let histogram = convertValuesToHistogram(values, bucketSize, 1, 29); + const histogram = convertValuesToHistogram(values, bucketSize, 1, 29); expect(histogram).toMatchObject(expected); }); it('Should not add empty buckets', () => { bucketSize = 5; - let expected = [[0, 2], [5, 0], [10, 2], [15, 1], [20, 1], [25, 1]]; + const expected = [[0, 2], [5, 0], [10, 2], [15, 1], [20, 1], [25, 1]]; - let histogram = convertValuesToHistogram(values, bucketSize, 1, 29); + const histogram = convertValuesToHistogram(values, bucketSize, 1, 29); expect(histogram).toMatchObject(expected); }); }); @@ -38,18 +38,18 @@ describe('Graph Histogam Converter', function() { }); it('Should convert to values array', () => { - let expected = [1, 2, 10, 11, 17, 20, 29]; + const expected = [1, 2, 10, 11, 17, 20, 29]; - let values = getSeriesValues(data); + const values = getSeriesValues(data); expect(values).toMatchObject(expected); }); it('Should skip null values', () => { data[0].datapoints.push([null, 0]); - let expected = [1, 2, 10, 11, 17, 20, 29]; + const expected = [1, 2, 10, 11, 17, 20, 29]; - let values = getSeriesValues(data); + const values = getSeriesValues(data); expect(values).toMatchObject(expected); }); }); diff --git a/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts b/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts index 2e7456a132a..40b6c1ba561 100644 --- a/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts +++ b/public/app/plugins/panel/graph/specs/series_override_ctrl.test.ts @@ -2,7 +2,7 @@ import '../series_overrides_ctrl'; import { SeriesOverridesCtrl } from '../series_overrides_ctrl'; describe('SeriesOverridesCtrl', () => { - let popoverSrv = {}; + const popoverSrv = {}; let $scope; beforeEach(() => { diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index cae1e5fac7f..84ecd2389b6 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -6,7 +6,7 @@ import { contextSrv } from 'app/core/core'; import { tickStep } from 'app/core/utils/ticks'; import { getColorScale, getOpacityScale } from './color_scale'; -let module = angular.module('grafana.directives'); +const module = angular.module('grafana.directives'); const LEGEND_HEIGHT_PX = 6; const LEGEND_WIDTH_PX = 100; @@ -21,8 +21,8 @@ module.directive('colorLegend', function() { restrict: 'E', template: '
    ', link: function(scope, elem, attrs) { - let ctrl = scope.ctrl; - let panel = scope.ctrl.panel; + const ctrl = scope.ctrl; + const panel = scope.ctrl.panel; render(); @@ -31,17 +31,17 @@ module.directive('colorLegend', function() { }); function render() { - let legendElem = $(elem).find('svg'); - let legendWidth = Math.floor(legendElem.outerWidth()); + const legendElem = $(elem).find('svg'); + const legendWidth = Math.floor(legendElem.outerWidth()); if (panel.color.mode === 'spectrum') { - let colorScheme = _.find(ctrl.colorSchemes, { + const colorScheme = _.find(ctrl.colorSchemes, { value: panel.color.colorScheme, }); - let colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, legendWidth); + const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, legendWidth); drawSimpleColorLegend(elem, colorScale); } else if (panel.color.mode === 'opacity') { - let colorOptions = panel.color; + const colorOptions = panel.color; drawSimpleOpacityLegend(elem, colorOptions); } } @@ -57,8 +57,8 @@ module.directive('heatmapLegend', function() { restrict: 'E', template: `
    `, link: function(scope, elem, attrs) { - let ctrl = scope.ctrl; - let panel = scope.ctrl.panel; + const ctrl = scope.ctrl; + const panel = scope.ctrl.panel; render(); ctrl.events.on('render', function() { @@ -68,18 +68,18 @@ module.directive('heatmapLegend', function() { function render() { clearLegend(elem); if (!_.isEmpty(ctrl.data) && !_.isEmpty(ctrl.data.cards)) { - let rangeFrom = 0; - let rangeTo = ctrl.data.cardStats.max; - let maxValue = panel.color.max || rangeTo; - let minValue = panel.color.min || 0; + const rangeFrom = 0; + const rangeTo = ctrl.data.cardStats.max; + const maxValue = panel.color.max || rangeTo; + const minValue = panel.color.min || 0; if (panel.color.mode === 'spectrum') { - let colorScheme = _.find(ctrl.colorSchemes, { + const colorScheme = _.find(ctrl.colorSchemes, { value: panel.color.colorScheme, }); drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue); } else if (panel.color.mode === 'opacity') { - let colorOptions = panel.color; + const colorOptions = panel.color; drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue); } } @@ -89,21 +89,21 @@ module.directive('heatmapLegend', function() { }); function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) { - let legendElem = $(elem).find('svg'); - let legend = d3.select(legendElem.get(0)); + const legendElem = $(elem).find('svg'); + const legend = d3.select(legendElem.get(0)); clearLegend(elem); - let legendWidth = Math.floor(legendElem.outerWidth()) - 30; - let legendHeight = legendElem.attr('height'); + const legendWidth = Math.floor(legendElem.outerWidth()) - 30; + const legendHeight = legendElem.attr('height'); let rangeStep = 1; if (rangeTo - rangeFrom > legendWidth) { rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth); } - let widthFactor = legendWidth / (rangeTo - rangeFrom); - let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); + const widthFactor = legendWidth / (rangeTo - rangeFrom); + const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); - let colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); + const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); legend .selectAll('.heatmap-color-legend-rect') .data(valuesRange) @@ -120,21 +120,21 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal } function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) { - let legendElem = $(elem).find('svg'); - let legend = d3.select(legendElem.get(0)); + const legendElem = $(elem).find('svg'); + const legend = d3.select(legendElem.get(0)); clearLegend(elem); - let legendWidth = Math.floor(legendElem.outerWidth()) - 30; - let legendHeight = legendElem.attr('height'); + const legendWidth = Math.floor(legendElem.outerWidth()) - 30; + const legendHeight = legendElem.attr('height'); let rangeStep = 1; if (rangeTo - rangeFrom > legendWidth) { rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth); } - let widthFactor = legendWidth / (rangeTo - rangeFrom); - let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); + const widthFactor = legendWidth / (rangeTo - rangeFrom); + const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); - let opacityScale = getOpacityScale(options, maxValue, minValue); + const opacityScale = getOpacityScale(options, maxValue, minValue); legend .selectAll('.heatmap-opacity-legend-rect') .data(valuesRange) @@ -152,27 +152,27 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue } function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) { - let legendElem = $(elem).find('svg'); - let legend = d3.select(legendElem.get(0)); + const legendElem = $(elem).find('svg'); + const legend = d3.select(legendElem.get(0)); if (legendWidth <= 0 || legendElem.get(0).childNodes.length === 0) { return; } - let legendValueScale = d3 + const legendValueScale = d3 .scaleLinear() .domain([0, rangeTo]) .range([0, legendWidth]); - let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); - let xAxis = d3 + const ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); + const xAxis = d3 .axisBottom(legendValueScale) .tickValues(ticks) .tickSize(LEGEND_TICK_SIZE); - let colorRect = legendElem.find(':first-child'); - let posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN; - let posX = getSvgElemX(colorRect); + const colorRect = legendElem.find(':first-child'); + const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN; + const posX = getSvgElemX(colorRect); d3 .select(legendElem.get(0)) @@ -188,18 +188,18 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal } function drawSimpleColorLegend(elem, colorScale) { - let legendElem = $(elem).find('svg'); + const legendElem = $(elem).find('svg'); clearLegend(elem); - let legendWidth = Math.floor(legendElem.outerWidth()); - let legendHeight = legendElem.attr('height'); + const legendWidth = Math.floor(legendElem.outerWidth()); + const legendHeight = legendElem.attr('height'); if (legendWidth) { - let valuesNumber = Math.floor(legendWidth / 2); - let rangeStep = Math.floor(legendWidth / valuesNumber); - let valuesRange = d3.range(0, legendWidth, rangeStep); + const valuesNumber = Math.floor(legendWidth / 2); + const rangeStep = Math.floor(legendWidth / valuesNumber); + const valuesRange = d3.range(0, legendWidth, rangeStep); - let legend = d3.select(legendElem.get(0)); + const legend = d3.select(legendElem.get(0)); var legendRects = legend.selectAll('.heatmap-color-legend-rect').data(valuesRange); legendRects @@ -215,12 +215,12 @@ function drawSimpleColorLegend(elem, colorScale) { } function drawSimpleOpacityLegend(elem, options) { - let legendElem = $(elem).find('svg'); + const legendElem = $(elem).find('svg'); clearLegend(elem); - let legend = d3.select(legendElem.get(0)); - let legendWidth = Math.floor(legendElem.outerWidth()); - let legendHeight = legendElem.attr('height'); + const legend = d3.select(legendElem.get(0)); + const legendWidth = Math.floor(legendElem.outerWidth()); + const legendHeight = legendElem.attr('height'); if (legendWidth) { let legendOpacityScale; @@ -237,8 +237,8 @@ function drawSimpleOpacityLegend(elem, options) { .range([0, 1]); } - let rangeStep = 10; - let valuesRange = d3.range(0, legendWidth, rangeStep); + const rangeStep = 10; + const valuesRange = d3.range(0, legendWidth, rangeStep); var legendRects = legend.selectAll('.heatmap-opacity-legend-rect').data(valuesRange); legendRects @@ -255,12 +255,12 @@ function drawSimpleOpacityLegend(elem, options) { } function clearLegend(elem) { - let legendElem = $(elem).find('svg'); + const legendElem = $(elem).find('svg'); legendElem.empty(); } function getSvgElemX(elem) { - let svgElem = elem.get(0); + const svgElem = elem.get(0); if (svgElem && svgElem.x && svgElem.x.baseVal) { return svgElem.x.baseVal.value; } else { @@ -269,7 +269,7 @@ function getSvgElemX(elem) { } function getSvgElemHeight(elem) { - let svgElem = elem.get(0); + const svgElem = elem.get(0); if (svgElem && svgElem.height && svgElem.height.baseVal) { return svgElem.height.baseVal.value; } else { @@ -278,13 +278,13 @@ function getSvgElemHeight(elem) { } function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { - let range = rangeTo - rangeFrom; - let tickStepSize = tickStep(rangeFrom, rangeTo, 3); - let ticksNum = Math.round(range / tickStepSize); + const range = rangeTo - rangeFrom; + const tickStepSize = tickStep(rangeFrom, rangeTo, 3); + const ticksNum = Math.round(range / tickStepSize); let ticks = []; for (let i = 0; i < ticksNum; i++) { - let current = tickStepSize * i; + const current = tickStepSize * i; // Add user-defined min and max if it had been set if (isValueCloseTo(minValue, current, tickStepSize)) { ticks.push(minValue); @@ -309,6 +309,6 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { } function isValueCloseTo(val, valueTo, step) { - let diff = Math.abs(val - valueTo); + const diff = Math.abs(val - valueTo); return diff < step * 0.3; } diff --git a/public/app/plugins/panel/heatmap/color_scale.ts b/public/app/plugins/panel/heatmap/color_scale.ts index 3550c981db2..2234deb8405 100644 --- a/public/app/plugins/panel/heatmap/color_scale.ts +++ b/public/app/plugins/panel/heatmap/color_scale.ts @@ -2,11 +2,11 @@ import * as d3 from 'd3'; import * as d3ScaleChromatic from 'd3-scale-chromatic'; export function getColorScale(colorScheme: any, lightTheme: boolean, maxValue: number, minValue = 0): (d: any) => any { - let colorInterpolator = d3ScaleChromatic[colorScheme.value]; - let colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark'); + const colorInterpolator = d3ScaleChromatic[colorScheme.value]; + const colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark'); - let start = colorScaleInverted ? maxValue : minValue; - let end = colorScaleInverted ? minValue : maxValue; + const start = colorScaleInverted ? maxValue : minValue; + const end = colorScaleInverted ? minValue : maxValue; return d3.scaleSequential(colorInterpolator).domain([start, end]); } diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index 1d35ff2ea84..66b72f8d37a 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -13,10 +13,10 @@ import { sortSeriesByLabel, } from './heatmap_data_converter'; -let X_BUCKET_NUMBER_DEFAULT = 30; -let Y_BUCKET_NUMBER_DEFAULT = 10; +const X_BUCKET_NUMBER_DEFAULT = 30; +const Y_BUCKET_NUMBER_DEFAULT = 10; -let panelDefaults = { +const panelDefaults = { heatmap: {}, cards: { cardPadding: null, @@ -57,12 +57,12 @@ let panelDefaults = { highlightCards: true, }; -let colorModes = ['opacity', 'spectrum']; -let opacityScales = ['linear', 'sqrt']; +const colorModes = ['opacity', 'spectrum']; +const opacityScales = ['linear', 'sqrt']; // Schemes from d3-scale-chromatic // https://github.com/d3/d3-scale-chromatic -let colorSchemes = [ +const colorSchemes = [ // Diverging { name: 'Spectral', value: 'interpolateSpectral', invert: 'always' }, { name: 'RdYlGn', value: 'interpolateRdYlGn', invert: 'always' }, @@ -161,11 +161,11 @@ export class HeatmapCtrl extends MetricsPanelCtrl { let xBucketSize, yBucketSize, bucketsData, heatmapStats; const logBase = this.panel.yAxis.logBase; - let xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; - let xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); + const xBucketNumber = this.panel.xBucketNumber || X_BUCKET_NUMBER_DEFAULT; + const xBucketSizeByNumber = Math.floor((this.range.to - this.range.from) / xBucketNumber); // Parse X bucket size (number or interval) - let isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); + const isIntervalString = kbn.interval_regex.test(this.panel.xBucketSize); if (isIntervalString) { xBucketSize = kbn.interval_to_ms(this.panel.xBucketSize); } else if ( @@ -180,7 +180,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { // Calculate Y bucket size heatmapStats = this.parseSeries(this.series); - let yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; + const yBucketNumber = this.panel.yBucketNumber || Y_BUCKET_NUMBER_DEFAULT; if (logBase !== 1) { yBucketSize = this.panel.yAxis.splitFactor; } else { @@ -204,7 +204,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { yBucketSize = 1; } - let { cards, cardStats } = convertToCards(bucketsData); + const { cards, cardStats } = convertToCards(bucketsData); this.data = { buckets: bucketsData, @@ -241,12 +241,12 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } // Calculate bucket size based on heatmap data - let xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); + const xBucketBoundSet = _.map(_.keys(bucketsData), key => Number(key)); xBucketSize = calculateBucketSize(xBucketBoundSet); // Always let yBucketSize=1 in 'tsbuckets' mode yBucketSize = 1; - let { cards, cardStats } = convertToCards(bucketsData); + const { cards, cardStats } = convertToCards(bucketsData); this.data = { buckets: bucketsData, @@ -284,7 +284,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { tip: 'No datapoints returned from data query', }; } else { - for (let series of this.series) { + for (const series of this.series) { if (series.isOutsideRange) { this.dataWarning = { title: 'Data points outside time range', @@ -313,17 +313,17 @@ export class HeatmapCtrl extends MetricsPanelCtrl { throw new Error('Heatmap error: data should be a time series'); } - let series = new TimeSeries({ + const series = new TimeSeries({ datapoints: seriesData.datapoints, alias: seriesData.target, }); series.flotpairs = series.getFlotPairs(this.panel.nullPointMode); - let datapoints = seriesData.datapoints || []; + const datapoints = seriesData.datapoints || []; if (datapoints && datapoints.length > 0) { - let last = datapoints[datapoints.length - 1][1]; - let from = this.range.from; + const last = datapoints[datapoints.length - 1][1]; + const from = this.range.from; if (last - from < -10000) { series.isOutsideRange = true; } @@ -333,9 +333,9 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } parseSeries(series) { - let min = _.min(_.map(series, s => s.stats.min)); - let minLog = _.min(_.map(series, s => s.stats.logmin)); - let max = _.max(_.map(series, s => s.stats.max)); + const min = _.min(_.map(series, s => s.stats.min)); + const minLog = _.min(_.map(series, s => s.stats.logmin)); + const max = _.max(_.map(series, s => s.stats.max)); return { max: max, @@ -345,10 +345,10 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } parseHistogramSeries(series) { - let bounds = _.map(series, s => Number(s.alias)); - let min = _.min(bounds); - let minLog = _.min(bounds); - let max = _.max(bounds); + const bounds = _.map(series, s => Number(s.alias)); + const min = _.min(bounds); + const minLog = _.min(bounds); + const max = _.max(bounds); return { max: max, diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index 048b19de911..0b3f83bbe46 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; -let VALUE_INDEX = 0; -let TIME_INDEX = 1; +const VALUE_INDEX = 0; +const TIME_INDEX = 1; interface XBucket { x: number; @@ -18,18 +18,18 @@ interface YBucket { * @param seriesList List of time series */ function histogramToHeatmap(seriesList) { - let heatmap = {}; + const heatmap = {}; for (let i = 0; i < seriesList.length; i++) { - let series = seriesList[i]; - let bound = i; + const series = seriesList[i]; + const bound = i; if (isNaN(bound)) { return heatmap; } - for (let point of series.datapoints) { - let count = point[VALUE_INDEX]; - let time = point[TIME_INDEX]; + for (const point of series.datapoints) { + const count = point[VALUE_INDEX]; + const time = point[TIME_INDEX]; if (!_.isNumber(count)) { continue; @@ -101,10 +101,10 @@ function parseHistogramLabel(label: string): number { function convertToCards(buckets) { let min = 0, max = 0; - let cards = []; + const cards = []; _.forEach(buckets, xBucket => { _.forEach(xBucket.buckets, yBucket => { - let card = { + const card = { x: xBucket.x, y: yBucket.y, yBounds: yBucket.bounds, @@ -123,7 +123,7 @@ function convertToCards(buckets) { }); }); - let cardStats = { min, max }; + const cardStats = { min, max }; return { cards, cardStats }; } @@ -146,19 +146,19 @@ function convertToCards(buckets) { */ function mergeZeroBuckets(buckets, minValue) { _.forEach(buckets, xBucket => { - let yBuckets = xBucket.buckets; + const yBuckets = xBucket.buckets; - let emptyBucket = { + const emptyBucket = { bounds: { bottom: 0, top: 0 }, values: [], points: [], count: 0, }; - let nullBucket = yBuckets[0] || emptyBucket; - let minBucket = yBuckets[minValue] || emptyBucket; + const nullBucket = yBuckets[0] || emptyBucket; + const minBucket = yBuckets[minValue] || emptyBucket; - let newBucket = { + const newBucket = { y: 0, bounds: { bottom: minValue, top: minBucket.bounds.top || minValue }, values: [], @@ -211,11 +211,11 @@ function mergeZeroBuckets(buckets, minValue) { * } */ function convertToHeatMap(seriesList, yBucketSize, xBucketSize, logBase = 1) { - let heatmap = {}; + const heatmap = {}; - for (let series of seriesList) { - let datapoints = series.datapoints; - let seriesName = series.label; + for (const series of seriesList) { + const datapoints = series.datapoints; + const seriesName = series.label; // Slice series into X axis buckets // | | ** | | * | **| @@ -224,7 +224,7 @@ function convertToHeatMap(seriesList, yBucketSize, xBucketSize, logBase = 1) { // |____|____|____|____|____|_ // _.forEach(datapoints, point => { - let bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize); + const bucketBound = getBucketBound(point[TIME_INDEX], xBucketSize); pushToXBuckets(heatmap, point, bucketBound, seriesName); }); } @@ -247,13 +247,13 @@ function convertToHeatMap(seriesList, yBucketSize, xBucketSize, logBase = 1) { } function pushToXBuckets(buckets, point, bucketNum, seriesName) { - let value = point[VALUE_INDEX]; + const value = point[VALUE_INDEX]; if (value === null || value === undefined || isNaN(value)) { return; } // Add series name to point for future identification - let point_ext = _.concat(point, seriesName); + const point_ext = _.concat(point, seriesName); if (buckets[bucketNum] && buckets[bucketNum].values) { buckets[bucketNum].values.push(value); @@ -308,18 +308,18 @@ function getBucketBounds(value, bucketSize) { } function getBucketBound(value, bucketSize) { - let bounds = getBucketBounds(value, bucketSize); + const bounds = getBucketBounds(value, bucketSize); return bounds.bottom; } function convertToValueBuckets(xBucket, bucketSize) { - let values = xBucket.values; - let points = xBucket.points; - let buckets = {}; + const values = xBucket.values; + const points = xBucket.points; + const buckets = {}; _.forEach(values, (val, index) => { - let bounds = getBucketBounds(val, bucketSize); - let bucketNum = bounds.bottom; + const bounds = getBucketBounds(val, bucketSize); + const bucketNum = bounds.bottom; pushToYBuckets(buckets, bucketNum, val, points[index], bounds); }); @@ -335,13 +335,13 @@ function getLogScaleBucketBounds(value, yBucketSplitFactor, logBase) { return { bottom: 0, top: 0 }; } - let value_log = logp(value, logBase); + const value_log = logp(value, logBase); let pow, powTop; if (yBucketSplitFactor === 1 || !yBucketSplitFactor) { pow = Math.floor(value_log); powTop = pow + 1; } else { - let additional_bucket_size = 1 / yBucketSplitFactor; + const additional_bucket_size = 1 / yBucketSplitFactor; let additional_log = value_log - Math.floor(value_log); additional_log = Math.floor(additional_log / additional_bucket_size) * additional_bucket_size; pow = Math.floor(value_log) + additional_log; @@ -354,18 +354,18 @@ function getLogScaleBucketBounds(value, yBucketSplitFactor, logBase) { } function getLogScaleBucketBound(value, yBucketSplitFactor, logBase) { - let bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase); + const bounds = getLogScaleBucketBounds(value, yBucketSplitFactor, logBase); return bounds.bottom; } function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) { - let values = xBucket.values; - let points = xBucket.points; + const values = xBucket.values; + const points = xBucket.points; - let buckets = {}; + const buckets = {}; _.forEach(values, (val, index) => { - let bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase); - let bucketNum = bounds.bottom; + const bounds = getLogScaleBucketBounds(val, yBucketSplitFactor, logBase); + const bucketNum = bounds.bottom; pushToYBuckets(buckets, bucketNum, val, points[index], bounds); }); @@ -396,7 +396,7 @@ function calculateBucketSize(bounds: number[], logBase = 1): number { } else { bounds = _.sortBy(bounds); for (let i = 1; i < bounds.length; i++) { - let distance = getDistance(bounds[i], bounds[i - 1], logBase); + const distance = getDistance(bounds[i], bounds[i - 1], logBase); bucketSize = distance < bucketSize ? distance : bucketSize; } } @@ -416,7 +416,7 @@ function getDistance(a: number, b: number, logBase = 1): number { return Math.abs(b - a); } else { // logarithmic distance - let ratio = Math.max(a, b) / Math.min(a, b); + const ratio = Math.max(a, b) / Math.min(a, b); return logp(ratio, logBase); } } diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 6cf9262f520..5e48849ca59 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -4,10 +4,10 @@ import _ from 'lodash'; import kbn from 'app/core/utils/kbn'; import { getValueBucketBound } from './heatmap_data_converter'; -let TOOLTIP_PADDING_X = 30; -let TOOLTIP_PADDING_Y = 5; -let HISTOGRAM_WIDTH = 160; -let HISTOGRAM_HEIGHT = 40; +const TOOLTIP_PADDING_X = 30; +const TOOLTIP_PADDING_Y = 5; +const HISTOGRAM_WIDTH = 160; +const HISTOGRAM_HEIGHT = 40; export class HeatmapTooltip { tooltip: any; @@ -67,7 +67,7 @@ export class HeatmapTooltip { return; } - let { xBucketIndex, yBucketIndex } = this.getBucketIndexes(pos, data); + const { xBucketIndex, yBucketIndex } = this.getBucketIndexes(pos, data); if (!data.buckets[xBucketIndex]) { this.destroy(); @@ -79,14 +79,14 @@ export class HeatmapTooltip { } let boundBottom, boundTop, valuesNumber; - let xData = data.buckets[xBucketIndex]; + const xData = data.buckets[xBucketIndex]; // Search in special 'zero' bucket also - let yData = _.find(xData.buckets, (bucket, bucketIndex) => { + const yData = _.find(xData.buckets, (bucket, bucketIndex) => { return bucket.bounds.bottom === yBucketIndex || bucketIndex === yBucketIndex.toString(); }); - let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; - let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); + const tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; + const time = this.dashboard.formatDate(xData.x, tooltipTimeFormat); // Decimals override. Code from panel/graph/graph.ts let countValueFormatter, bucketBoundFormatter; @@ -97,7 +97,7 @@ export class HeatmapTooltip { // auto decimals // legend and tooltip gets one more decimal precision // than graph legend ticks - let decimals = (this.panelCtrl.decimals || -1) + 1; + const decimals = (this.panelCtrl.decimals || -1) + 1; countValueFormatter = this.countValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); bucketBoundFormatter = this.panelCtrl.tickValueFormatter(decimals, this.panelCtrl.scaledDecimals + 2); } @@ -117,7 +117,7 @@ export class HeatmapTooltip { boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : ''; } else { // Display 0 if bucket is a special 'zero' bucket - let bottom = yData.y ? yData.bounds.bottom : 0; + const bottom = yData.y ? yData.bounds.bottom : 0; boundBottom = bucketBoundFormatter(bottom); boundTop = bucketBoundFormatter(yData.bounds.top); } @@ -158,7 +158,7 @@ export class HeatmapTooltip { 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 => { + const xBucket = _.find(data.buckets, bucket => { return x > bucket.x && x - bucket.x <= data.xBucketSize; }); return xBucket ? xBucket.x : getValueBucketBound(x, data.xBucketSize, 1); @@ -168,7 +168,7 @@ export class HeatmapTooltip { if (data.tsBuckets) { return Math.floor(y); } - let yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase); + const yBucketIndex = getValueBucketBound(y, data.yBucketSize, this.panel.yAxis.logBase); return yBucketIndex; } @@ -180,8 +180,8 @@ export class HeatmapTooltip { } addHistogram(data) { - let xBucket = this.scope.ctrl.data.buckets[data.x]; - let yBucketSize = this.scope.ctrl.data.yBucketSize; + const xBucket = this.scope.ctrl.data.buckets[data.x]; + const yBucketSize = this.scope.ctrl.data.yBucketSize; let min, max, ticks; if (this.scope.ctrl.data.tsBuckets) { min = 0; @@ -193,33 +193,33 @@ export class HeatmapTooltip { ticks = this.scope.ctrl.data.yAxis.ticks; } let histogramData = _.map(xBucket.buckets, bucket => { - let count = bucket.count !== undefined ? bucket.count : bucket.values.length; + const count = bucket.count !== undefined ? bucket.count : bucket.values.length; return [bucket.bounds.bottom, count]; }); histogramData = _.filter(histogramData, d => { return d[0] >= min && d[0] <= max; }); - let scale = this.scope.yScale.copy(); - let histXScale = scale.domain([min, max]).range([0, HISTOGRAM_WIDTH]); + const scale = this.scope.yScale.copy(); + const histXScale = scale.domain([min, max]).range([0, HISTOGRAM_WIDTH]); let barWidth; if (this.panel.yAxis.logBase === 1) { barWidth = Math.floor(HISTOGRAM_WIDTH / (max - min) * yBucketSize * 0.9); } else { - let barNumberFactor = yBucketSize ? yBucketSize : 1; + const barNumberFactor = yBucketSize ? yBucketSize : 1; barWidth = Math.floor(HISTOGRAM_WIDTH / ticks / barNumberFactor * 0.9); } barWidth = Math.max(barWidth, 1); // Normalize histogram Y axis - let histogramDomain = _.reduce(_.map(histogramData, d => d[1]), (sum, val) => sum + val, 0); - let histYScale = d3 + const histogramDomain = _.reduce(_.map(histogramData, d => d[1]), (sum, val) => sum + val, 0); + const histYScale = d3 .scaleLinear() .domain([0, histogramDomain]) .range([0, HISTOGRAM_HEIGHT]); - let histogram = this.tooltip + const histogram = this.tooltip .select('.heatmap-histogram') .append('svg') .attr('width', HISTOGRAM_WIDTH) @@ -247,9 +247,9 @@ export class HeatmapTooltip { return; } - let elem = $(this.tooltip.node())[0]; - let tooltipWidth = elem.clientWidth; - let tooltipHeight = elem.clientHeight; + const elem = $(this.tooltip.node())[0]; + const tooltipWidth = elem.clientWidth; + const tooltipHeight = elem.clientHeight; let left = pos.pageX + TOOLTIP_PADDING_X; let top = pos.pageY + TOOLTIP_PADDING_Y; @@ -266,7 +266,7 @@ export class HeatmapTooltip { } countValueFormatter(decimals, scaledDecimals = null) { - let format = 'short'; + const format = 'short'; return function(value) { return kbn.valueFormats[format](value, decimals, scaledDecimals); }; diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 8ea216be89d..fcbb39f8417 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -9,7 +9,7 @@ import { HeatmapTooltip } from './heatmap_tooltip'; import { mergeZeroBuckets } from './heatmap_data_converter'; import { getColorScale, getOpacityScale } from './color_scale'; -let MIN_CARD_SIZE = 1, +const MIN_CARD_SIZE = 1, CARD_PADDING = 1, CARD_ROUND = 0, DATA_RANGE_WIDING_FACTOR = 1.2, @@ -117,8 +117,8 @@ export class HeatmapRenderer { } getYAxisWidth(elem) { - let axis_text = elem.selectAll('.axis-y text').nodes(); - let max_text_width = _.max( + const axis_text = elem.selectAll('.axis-y text').nodes(); + const max_text_width = _.max( _.map(axis_text, text => { // Use SVG getBBox method return text.getBBox().width; @@ -129,10 +129,10 @@ export class HeatmapRenderer { } getXAxisHeight(elem) { - let axis_line = elem.select('.axis-x line'); + const axis_line = elem.select('.axis-x line'); if (!axis_line.empty()) { - let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2')); - let canvas_width = parseFloat(elem.attr('height')); + const axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2')); + const canvas_width = parseFloat(elem.attr('height')); return canvas_width - axis_line_position; } else { // Default height @@ -146,25 +146,25 @@ export class HeatmapRenderer { .domain([this.timeRange.from, this.timeRange.to]) .range([0, this.chartWidth]); - let ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX; - let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); + const ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX; + const grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); let timeFormat; - let dashboardTimeZone = this.ctrl.dashboard.getTimezone(); + const dashboardTimeZone = this.ctrl.dashboard.getTimezone(); if (dashboardTimeZone === 'utc') { timeFormat = d3.utcFormat(grafanaTimeFormatter); } else { timeFormat = d3.timeFormat(grafanaTimeFormatter); } - let xAxis = d3 + const xAxis = d3 .axisBottom(this.xScale) .ticks(ticks) .tickFormat(timeFormat) .tickPadding(X_AXIS_TICK_PADDING) .tickSize(this.chartHeight); - let posY = this.margin.top; - let posX = this.yAxisWidth; + const posY = this.margin.top; + const posX = this.yAxisWidth; this.heatmap .append('g') .attr('class', 'axis axis-x') @@ -191,11 +191,11 @@ export class HeatmapRenderer { tick_interval = ticksUtils.tickStep(y_min, y_max, ticks); ticks = Math.ceil((y_max - y_min) / tick_interval); - let decimalsAuto = ticksUtils.getPrecision(tick_interval); + const decimalsAuto = ticksUtils.getPrecision(tick_interval); 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); + const flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto); + const scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); this.ctrl.decimals = decimals; this.ctrl.scaledDecimals = scaledDecimals; @@ -218,7 +218,7 @@ export class HeatmapRenderer { .domain([y_min, y_max]) .range([this.chartHeight, 0]); - let yAxis = d3 + const yAxis = d3 .axisLeft(this.yScale) .ticks(ticks) .tickFormat(this.tickValueFormatter(decimals, scaledDecimals)) @@ -232,8 +232,8 @@ export class HeatmapRenderer { .call(yAxis); // Calculate Y axis width first, then move axis into visible area - let posY = this.margin.top; - let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + 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) @@ -245,7 +245,7 @@ export class HeatmapRenderer { // Wide Y values range and anjust to bucket size wideYAxisRange(min, max, tickInterval) { - let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2; + const y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2; let y_min, y_max; if (tickInterval === 0) { @@ -266,7 +266,7 @@ export class HeatmapRenderer { } addLogYAxis() { - let log_base = this.panel.yAxis.logBase; + const 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 = @@ -285,15 +285,15 @@ export class HeatmapRenderer { .domain([y_min, y_max]) .range([this.chartHeight, 0]); - let domain = this.yScale.domain(); - let tick_values = this.logScaleTickValues(domain, log_base); + const domain = this.yScale.domain(); + const tick_values = this.logScaleTickValues(domain, log_base); - let decimalsAuto = ticksUtils.getPrecision(y_min); - let decimals = this.panel.yAxis.decimals || decimalsAuto; + const decimalsAuto = ticksUtils.getPrecision(y_min); + const 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); + const flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto); + const scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size); this.ctrl.decimals = decimals; this.ctrl.scaledDecimals = scaledDecimals; @@ -303,7 +303,7 @@ export class HeatmapRenderer { ticks: tick_values.length, }; - let yAxis = d3 + const yAxis = d3 .axisLeft(this.yScale) .tickValues(tick_values) .tickFormat(this.tickValueFormatter(decimals, scaledDecimals)) @@ -317,8 +317,8 @@ export class HeatmapRenderer { .call(yAxis); // Calculate Y axis width first, then move axis into visible area - let posY = this.margin.top; - let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING; + 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 + ')'); // Set first tick as pseudo 0 @@ -349,7 +349,7 @@ export class HeatmapRenderer { const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals; this.ctrl.decimals = decimals; - let tickValueFormatter = this.tickValueFormatter.bind(this); + const tickValueFormatter = this.tickValueFormatter.bind(this); function tickFormatter(valIndex) { let valueFormatted = tsBuckets[valIndex]; if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') { @@ -362,7 +362,7 @@ export class HeatmapRenderer { const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i)); this.data.tsBucketsFormatted = tsBucketsFormatted; - let yAxis = d3 + const yAxis = d3 .axisLeft(this.yScale) .tickValues(tick_values) .tickFormat(tickFormatter) @@ -413,21 +413,21 @@ export class HeatmapRenderer { } logScaleTickValues(domain, base) { - let domainMin = domain[0]; - let domainMax = domain[1]; - let tickValues = []; + const domainMin = domain[0]; + const domainMax = domain[1]; + const tickValues = []; if (domainMin < 1) { - let under_one_ticks = Math.floor(ticksUtils.logp(domainMin, base)); + const under_one_ticks = Math.floor(ticksUtils.logp(domainMin, base)); for (let i = under_one_ticks; i < 0; i++) { - let tick_value = Math.pow(base, i); + const tick_value = Math.pow(base, i); tickValues.push(tick_value); } } - let ticks = Math.ceil(ticksUtils.logp(domainMax, base)); + const ticks = Math.ceil(ticksUtils.logp(domainMax, base)); for (let i = 0; i <= ticks; i++) { - let tick_value = Math.pow(base, i); + const tick_value = Math.pow(base, i); tickValues.push(tick_value); } @@ -435,7 +435,7 @@ export class HeatmapRenderer { } tickValueFormatter(decimals, scaledDecimals = null) { - let format = this.panel.yAxis.format; + const format = this.panel.yAxis.format; return function(value) { try { return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value; @@ -490,7 +490,7 @@ export class HeatmapRenderer { } addHeatmapCanvas() { - let heatmap_elem = this.$heatmap[0]; + const heatmap_elem = this.$heatmap[0]; this.width = Math.floor(this.$heatmap.width()) - this.padding.right; this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom; @@ -514,18 +514,18 @@ export class HeatmapRenderer { this.addAxes(); 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); + const log_base = this.panel.yAxis.logBase; + const domain = this.yScale.domain(); + const tick_values = this.logScaleTickValues(domain, log_base); this.data.buckets = mergeZeroBuckets(this.data.buckets, _.min(tick_values)); } - 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; + const cardsData = this.data.cards; + const maxValueAuto = this.data.cardStats.max; + const maxValue = this.panel.color.max || maxValueAuto; + const minValue = this.panel.color.min || 0; - let colorScheme = _.find(this.ctrl.colorSchemes, { + const colorScheme = _.find(this.ctrl.colorSchemes, { value: this.panel.color.colorScheme, }); this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue); @@ -549,7 +549,7 @@ export class HeatmapRenderer { .style('stroke-width', 0) .style('opacity', this.getCardOpacity.bind(this)); - let $cards = this.$heatmap.find('.heatmap-card'); + const $cards = this.$heatmap.find('.heatmap-card'); $cards .on('mouseenter', event => { this.tooltip.mouseOverBucket = true; @@ -562,10 +562,10 @@ export class HeatmapRenderer { } 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); + const color = d3.select(event.target).style('fill'); + const highlightColor = d3.color(color).darker(2); + const strokeColor = d3.color(color).brighter(4); + const current_card = d3.select(event.target); this.tooltip.originalFillColor = color; current_card .style('fill', highlightColor.toString()) @@ -582,12 +582,12 @@ export class HeatmapRenderer { } setCardSize() { - let xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0)); + const 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 (this.panel.yAxis.logBase !== 1) { - let base = this.panel.yAxis.logBase; - let splitFactor = this.data.yBucketSize || 1; + const base = this.panel.yAxis.logBase; + const splitFactor = this.data.yBucketSize || 1; yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor); } @@ -611,7 +611,7 @@ export class HeatmapRenderer { let w; if (this.xScale(d.x) < 0) { // Cut card left to prevent overlay - let cutted_width = this.xScale(d.x) + this.cardWidth; + const cutted_width = this.xScale(d.x) + this.cardWidth; w = cutted_width > 0 ? cutted_width : 0; } else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) { // Cut card right to prevent overlay @@ -639,7 +639,7 @@ export class HeatmapRenderer { } getCardHeight(d) { - let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding; + const y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding; let h = this.cardHeight; if (this.panel.yAxis.logBase !== 1 && d.y === 0) { @@ -703,10 +703,10 @@ export class HeatmapRenderer { this.mouseUpHandler = null; this.selection.active = false; - let selectionRange = Math.abs(this.selection.x2 - this.selection.x1); + const 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); + const timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth); + const timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth); this.ctrl.timeSrv.setTime({ from: moment.utc(timeFrom), @@ -744,9 +744,9 @@ export class HeatmapRenderer { } getEventPos(event, offset) { - let x = this.xScale.invert(offset.x - this.yAxisWidth).valueOf(); - let y = this.yScale.invert(offset.y - this.chartTop); - let pos = { + const x = this.xScale.invert(offset.x - this.yAxisWidth).valueOf(); + const y = this.yScale.invert(offset.y - this.chartTop); + const pos = { pageX: event.pageX, pageY: event.pageY, x: x, @@ -776,8 +776,8 @@ export class HeatmapRenderer { drawSelection(posX1, posX2) { if (this.heatmap) { this.heatmap.selectAll('.heatmap-selection').remove(); - let selectionX = Math.min(posX1, posX2); - let selectionWidth = Math.abs(posX1 - posX2); + const selectionX = Math.min(posX1, posX2); + const selectionWidth = Math.abs(posX1 - posX2); if (selectionWidth > MIN_SELECTION_WIDTH) { this.heatmap @@ -823,7 +823,7 @@ export class HeatmapRenderer { drawSharedCrosshair(pos) { if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) { - let posX = this.xScale(pos.x) + this.yAxisWidth; + const posX = this.xScale(pos.x) + this.yAxisWidth; this.drawCrosshair(posX); } } diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts index 800c2518f9a..d9d929a2697 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts @@ -2,13 +2,13 @@ import moment from 'moment'; import { HeatmapCtrl } from '../heatmap_ctrl'; describe('HeatmapCtrl', function() { - let ctx = {}; + const ctx = {}; - let $injector = { + const $injector = { get: () => {}, }; - let $scope = { + const $scope = { $on: () => {}, }; diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts index b6a8713a3e9..1c8a7a32caf 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_data_converter.test.ts @@ -10,7 +10,7 @@ import { } from '../heatmap_data_converter'; describe('isHeatmapDataEqual', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.heatmapA = { @@ -35,17 +35,17 @@ describe('isHeatmapDataEqual', () => { }); it('should proper compare objects', () => { - let heatmapC = _.cloneDeep(ctx.heatmapA); + const heatmapC = _.cloneDeep(ctx.heatmapA); heatmapC['1422774000000'].buckets['1'].values = [1, 1.5]; - let heatmapD = _.cloneDeep(ctx.heatmapA); + const heatmapD = _.cloneDeep(ctx.heatmapA); heatmapD['1422774000000'].buckets['1'].values = [1.5, 1, 1.6]; - let heatmapE = _.cloneDeep(ctx.heatmapA); + const heatmapE = _.cloneDeep(ctx.heatmapA); heatmapE['1422774000000'].buckets['1'].values = [1, 1.6]; - let empty = {}; - let emptyValues = _.cloneDeep(ctx.heatmapA); + const empty = {}; + const emptyValues = _.cloneDeep(ctx.heatmapA); emptyValues['1422774000000'].buckets['1'].values = []; expect(isHeatmapDataEqual(ctx.heatmapA, ctx.heatmapB)).toBe(true); @@ -69,7 +69,7 @@ describe('isHeatmapDataEqual', () => { }); describe('calculateBucketSize', () => { - let ctx: any = {}; + const ctx: any = {}; describe('when logBase is 1 (linear scale)', () => { beforeEach(() => { @@ -88,7 +88,7 @@ describe('calculateBucketSize', () => { it('should properly calculate bucket size', () => { _.each(ctx.bounds_set, b => { - let bucketSize = calculateBucketSize(b.bounds, ctx.logBase); + const bucketSize = calculateBucketSize(b.bounds, ctx.logBase); expect(bucketSize).toBe(b.size); }); }); @@ -108,7 +108,7 @@ describe('calculateBucketSize', () => { it('should properly calculate bucket size', () => { _.each(ctx.bounds_set, b => { - let bucketSize = calculateBucketSize(b.bounds, ctx.logBase); + const bucketSize = calculateBucketSize(b.bounds, ctx.logBase); expect(isEqual(bucketSize, b.size)).toBe(true); }); }); @@ -116,7 +116,7 @@ describe('calculateBucketSize', () => { }); describe('HeatmapDataConverter', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.series = []; @@ -150,7 +150,7 @@ describe('HeatmapDataConverter', () => { }); it('should build proper heatmap data', () => { - let expectedHeatmap = { + const expectedHeatmap = { '1422774000000': { x: 1422774000000, buckets: { @@ -183,7 +183,7 @@ describe('HeatmapDataConverter', () => { }, }; - let heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); + const heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).toBe(true); }); }); @@ -194,7 +194,7 @@ describe('HeatmapDataConverter', () => { }); it('should build proper heatmap data', () => { - let expectedHeatmap = { + const expectedHeatmap = { '1422774000000': { x: 1422774000000, buckets: { @@ -210,14 +210,14 @@ describe('HeatmapDataConverter', () => { }, }; - let heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); + const heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase); expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).toBe(true); }); }); }); describe('Histogram converter', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.series = []; @@ -248,7 +248,7 @@ describe('Histogram converter', () => { beforeEach(() => {}); it('should build proper heatmap data', () => { - let expectedHeatmap = { + const expectedHeatmap = { '1422774000000': { x: 1422774000000, buckets: { @@ -343,18 +343,18 @@ describe('convertToCards', () => { }); it('should build proper cards data', () => { - let expectedCards = [ + const expectedCards = [ { x: 1422774000000, y: 1, count: 1, values: [1], yBounds: {} }, { x: 1422774000000, y: 2, count: 1, values: [2], yBounds: {} }, { x: 1422774060000, y: 2, count: 2, values: [2, 3], yBounds: {} }, ]; - let res = convertToCards(buckets); + const res = convertToCards(buckets); expect(res.cards).toMatchObject(expectedCards); }); it('should build proper cards stats', () => { - let expectedStats = { min: 1, max: 2 }; - let res = convertToCards(buckets); + const expectedStats = { min: 1, max: 2 }; + const res = convertToCards(buckets); expect(res.cardStats).toMatchObject(expectedStats); }); }); diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index acfa69b171c..93bf258f50d 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -60,7 +60,7 @@ class PluginListCtrl extends PanelCtrl { this.viewModel[1].list = _.filter(plugins, { type: 'panel' }); this.viewModel[2].list = _.filter(plugins, { type: 'datasource' }); - for (let plugin of this.pluginList) { + for (const plugin of this.pluginList) { if (plugin.hasUpdate) { plugin.state = 'has-update'; } else if (!plugin.enabled) { diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index ebd2628b086..b858f77556f 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -293,8 +293,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { } if (this.series && this.series.length > 0) { - let lastPoint = _.last(this.series[0].datapoints); - let lastValue = _.isArray(lastPoint) ? lastPoint[0] : null; + const lastPoint = _.last(this.series[0].datapoints); + const lastValue = _.isArray(lastPoint) ? lastPoint[0] : null; if (this.panel.valueName === 'name') { data.value = 0; @@ -305,7 +305,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { data.valueFormatted = _.escape(lastValue); data.valueRounded = 0; } else if (this.panel.valueName === 'last_time') { - let formatFunc = kbn.valueFormats[this.panel.format]; + const formatFunc = kbn.valueFormats[this.panel.format]; data.value = lastPoint[1]; data.valueRounded = data.value; data.valueFormatted = formatFunc(data.value, this.dashboard.isTimezoneUtc()); @@ -313,8 +313,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { data.value = this.series[0].stats[this.panel.valueName]; data.flotpairs = this.series[0].flotpairs; - let decimalInfo = this.getDecimalsForValue(data.value); - let formatFunc = kbn.valueFormats[this.panel.format]; + const decimalInfo = this.getDecimalsForValue(data.value); + const formatFunc = kbn.valueFormats[this.panel.format]; data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); } @@ -330,7 +330,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { // check value to text mappings if its enabled if (this.panel.mappingType === 1) { for (let i = 0; i < this.panel.valueMaps.length; i++) { - let map = this.panel.valueMaps[i]; + const map = this.panel.valueMaps[i]; // special null case if (map.value === 'null') { if (data.value === null || data.value === void 0) { @@ -349,7 +349,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { } } else if (this.panel.mappingType === 2) { for (let i = 0; i < this.panel.rangeMaps.length; i++) { - let map = this.panel.rangeMaps[i]; + const map = this.panel.rangeMaps[i]; // special null case if (map.from === 'null' && map.to === 'null') { if (data.value === null || data.value === void 0) { diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts index 0480d0be5c3..9d204f19f5b 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts @@ -2,15 +2,15 @@ import { SingleStatCtrl } from '../module'; import moment from 'moment'; describe('SingleStatCtrl', function() { - let ctx = {}; - let epoch = 1505826363746; + const ctx = {}; + const epoch = 1505826363746; Date.now = () => epoch; - let $scope = { + const $scope = { $on: () => {}, }; - let $injector = { + const $injector = { get: () => {}, }; diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 03d92f7e48f..4169a25dd43 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -243,7 +243,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { }); function addFilterClicked(e) { - let filterData = $(e.currentTarget).data(); + const filterData = $(e.currentTarget).data(); var options = { datasource: panel.datasource, key: data.columns[filterData.column].text, diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index d85c20a87cc..d512c1335df 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -21,11 +21,11 @@ export class TableRenderer { this.colorState = {}; for (let colIndex = 0; colIndex < this.table.columns.length; colIndex++) { - let column = this.table.columns[colIndex]; + const column = this.table.columns[colIndex]; column.title = column.text; for (let i = 0; i < this.panel.styles.length; i++) { - let style = this.panel.styles[i]; + const style = this.panel.styles[i]; var regex = kbn.stringToJsRegex(style.pattern); if (column.text.match(regex)) { @@ -154,7 +154,7 @@ export class TableRenderer { } if (column.style.type === 'number') { - let valueFormatter = kbn.valueFormats[column.unit || column.style.unit]; + const valueFormatter = kbn.valueFormats[column.unit || column.style.unit]; return v => { if (v === null || v === void 0) { @@ -193,9 +193,9 @@ export class TableRenderer { } renderRowVariables(rowIndex) { - let scopedVars = {}; + const scopedVars = {}; let cell_variable; - let row = this.table.rows[rowIndex]; + const row = this.table.rows[rowIndex]; for (let i = 0; i < row.length; i++) { cell_variable = `__cell_${i}`; scopedVars[cell_variable] = { value: row[i] }; @@ -288,15 +288,15 @@ export class TableRenderer { } render(page) { - let pageSize = this.panel.pageSize || 100; - let startPos = page * pageSize; - let endPos = Math.min(startPos + pageSize, this.table.rows.length); + const pageSize = this.panel.pageSize || 100; + const startPos = page * pageSize; + const endPos = Math.min(startPos + pageSize, this.table.rows.length); var html = ''; - let rowClasses = []; + const rowClasses = []; let rowClass = ''; for (var y = startPos; y < endPos; y++) { - let row = this.table.rows[y]; + const row = this.table.rows[y]; let cellHtml = ''; let rowStyle = ''; for (var i = 0; i < this.table.columns.length; i++) { @@ -320,11 +320,11 @@ export class TableRenderer { } render_values() { - let rows = []; + const rows = []; for (var y = 0; y < this.table.rows.length; y++) { - let row = this.table.rows[y]; - let new_row = []; + const row = this.table.rows[y]; + const new_row = []; for (var i = 0; i < this.table.columns.length; i++) { new_row.push(this.formatColumnValue(i, row[i])); } diff --git a/public/app/plugins/panel/table/transformers.ts b/public/app/plugins/panel/table/transformers.ts index 1659ba3e3aa..840bfa83d5b 100644 --- a/public/app/plugins/panel/table/transformers.ts +++ b/public/app/plugins/panel/table/transformers.ts @@ -294,7 +294,7 @@ transformers['json'] = { transform: function(data, panel, model) { var i, y, z; - for (let column of panel.columns) { + for (const column of panel.columns) { var tableCol: any = { text: column.text }; // if filterable data then set columns to filterable diff --git a/public/app/stores/AlertListStore/AlertListStore.ts b/public/app/stores/AlertListStore/AlertListStore.ts index ec27565a1a1..c2b9f5e4962 100644 --- a/public/app/stores/AlertListStore/AlertListStore.ts +++ b/public/app/stores/AlertListStore/AlertListStore.ts @@ -13,7 +13,7 @@ export const AlertListStore = types }) .views(self => ({ get filteredRules() { - let regex = new RegExp(self.search, 'i'); + const regex = new RegExp(self.search, 'i'); return self.rules.filter(alert => { return regex.test(alert.name) || regex.test(alert.stateText) || regex.test(alert.info); }); @@ -26,7 +26,7 @@ export const AlertListStore = types const apiRules = yield backendSrv.get('/api/alerts', filters); self.rules.clear(); - for (let rule of apiRules) { + for (const rule of apiRules) { setStateFields(rule, rule.state); if (rule.state !== 'paused') { diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index bef53b828b6..d869b0f740d 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -12,9 +12,9 @@ export const NavStore = types load(...args) { let children = getEnv(self).navTree; let main, node; - let parents = []; + const parents = []; - for (let id of args) { + for (const id of args) { node = children.find(el => el.id === id); if (!node) { @@ -28,7 +28,7 @@ export const NavStore = types main = parents[parents.length - 2]; if (main.children) { - for (let item of main.children) { + for (const item of main.children) { item.active = false; if (item.url === node.url) { @@ -42,7 +42,7 @@ export const NavStore = types }, initFolderNav(folder: any, activeChildId: string) { - let main = { + const main = { icon: 'fa fa-folder-open', id: 'manage-folder', subTitle: 'Manage folder dashboards & permissions', @@ -79,13 +79,13 @@ export const NavStore = types initDatasourceEditNav(ds: any, plugin: any, currentPage: string) { let title = 'New'; - let subTitle = `Type: ${plugin.name}`; + const subTitle = `Type: ${plugin.name}`; if (ds.id) { title = ds.name; } - let main = { + const main = { img: plugin.info.logos.large, id: 'ds-edit-' + plugin.id, subTitle: subTitle, @@ -118,7 +118,7 @@ export const NavStore = types }, initTeamPage(team: Team, tab: string, isSyncEnabled: boolean) { - let main = { + const main = { img: team.avatarUrl, id: 'team-' + team.id, subTitle: 'Manage members & settings', diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index 95d63c8527a..d778a09443d 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -117,7 +117,7 @@ export const PermissionsStore = types }), addStoreItem: flow(function* addStoreItem() { - let item = { + const item = { type: self.newItem.type, permission: self.newItem.permission, dashboardId: self.dashboardId, @@ -155,7 +155,7 @@ export const PermissionsStore = types try { yield updateItems(self, updatedItems); self.items.push(newItem); - let sortedItems = self.items.sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); + const sortedItems = self.items.sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); self.items = sortedItems; resetNewTypeInternal(); } catch {} @@ -197,7 +197,7 @@ export const PermissionsStore = types const updateItems = (self, items) => { const backendSrv = getEnv(self).backendSrv; const updated = []; - for (let item of items) { + for (const item of items) { if (item.inherited) { continue; } diff --git a/public/app/stores/TeamsStore/TeamsStore.ts b/public/app/stores/TeamsStore/TeamsStore.ts index 1aec4a1433c..f8e101163a2 100644 --- a/public/app/stores/TeamsStore/TeamsStore.ts +++ b/public/app/stores/TeamsStore/TeamsStore.ts @@ -32,8 +32,8 @@ export const TeamModel = types }) .views(self => ({ get filteredMembers() { - let members = this.members.values(); - let regex = new RegExp(self.search, 'i'); + const members = this.members.values(); + const regex = new RegExp(self.search, 'i'); return members.filter(member => { return regex.test(member.login) || regex.test(member.email); }); @@ -66,7 +66,7 @@ export const TeamModel = types const rsp = yield backendSrv.get(`/api/teams/${self.id}/members`); self.members.clear(); - for (let member of rsp) { + for (const member of rsp) { self.members.set(member.userId.toString(), TeamMemberModel.create(member)); } }), @@ -88,7 +88,7 @@ export const TeamModel = types const rsp = yield backendSrv.get(`/api/teams/${self.id}/groups`); self.groups.clear(); - for (let group of rsp) { + for (const group of rsp) { self.groups.set(group.groupId, TeamGroupModel.create(group)); } }), @@ -122,8 +122,8 @@ export const TeamsStore = types }) .views(self => ({ get filteredTeams() { - let teams = this.map.values(); - let regex = new RegExp(self.search, 'i'); + const teams = this.map.values(); + const regex = new RegExp(self.search, 'i'); return teams.filter(team => { return regex.test(team.name); }); @@ -135,7 +135,7 @@ export const TeamsStore = types const rsp = yield backendSrv.get('/api/teams/search/', { perpage: 50, page: 1 }); self.map.clear(); - for (let team of rsp.teams) { + for (const team of rsp.teams) { self.map.set(team.id.toString(), TeamModel.create(team)); } }), diff --git a/public/app/stores/ViewStore/ViewStore.ts b/public/app/stores/ViewStore/ViewStore.ts index ba966a194d8..3af6737209c 100644 --- a/public/app/stores/ViewStore/ViewStore.ts +++ b/public/app/stores/ViewStore/ViewStore.ts @@ -25,7 +25,7 @@ export const ViewStore = types // querystring only function updateQuery(query: any) { self.query.clear(); - for (let key of Object.keys(query)) { + for (const key of Object.keys(query)) { if (query[key]) { self.query.set(key, query[key]); } @@ -35,7 +35,7 @@ export const ViewStore = types // needed to get route parameters like slug from the url function updateRouteParams(routeParams: any) { self.routeParams.clear(); - for (let key of Object.keys(routeParams)) { + for (const key of Object.keys(routeParams)) { if (routeParams[key]) { self.routeParams.set(key, routeParams[key]); } diff --git a/public/test/core/utils/version_test.ts b/public/test/core/utils/version_test.ts index 20983cb32ec..91330389e24 100644 --- a/public/test/core/utils/version_test.ts +++ b/public/test/core/utils/version_test.ts @@ -1,11 +1,11 @@ -import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version'; +import { SemVersion, isVersionGtOrEq } from 'app/core/utils/version'; -describe("SemVersion", () => { +describe('SemVersion', () => { let version = '1.0.0-alpha.1'; describe('parsing', () => { it('should parse version properly', () => { - let semver = new SemVersion(version); + const semver = new SemVersion(version); expect(semver.major).toBe(1); expect(semver.minor).toBe(0); expect(semver.patch).toBe(0); @@ -19,15 +19,15 @@ describe("SemVersion", () => { }); it('should detect greater version properly', () => { - let semver = new SemVersion(version); - let cases = [ - {value: '3.4.5', expected: true}, - {value: '3.4.4', expected: true}, - {value: '3.4.6', expected: false}, - {value: '4', expected: false}, - {value: '3.5', expected: false}, + const semver = new SemVersion(version); + const cases = [ + { value: '3.4.5', expected: true }, + { value: '3.4.4', expected: true }, + { value: '3.4.6', expected: false }, + { value: '4', expected: false }, + { value: '3.5', expected: false }, ]; - cases.forEach((testCase) => { + cases.forEach(testCase => { expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected); }); }); @@ -35,17 +35,17 @@ describe("SemVersion", () => { describe('isVersionGtOrEq', () => { it('should compare versions properly (a >= b)', () => { - let cases = [ - {values: ['3.4.5', '3.4.5'], expected: true}, - {values: ['3.4.5', '3.4.4'] , expected: true}, - {values: ['3.4.5', '3.4.6'], expected: false}, - {values: ['3.4', '3.4.0'], expected: true}, - {values: ['3', '3.0.0'], expected: true}, - {values: ['3.1.1-beta1', '3.1'], expected: true}, - {values: ['3.4.5', '4'], expected: false}, - {values: ['3.4.5', '3.5'], expected: false}, + const cases = [ + { values: ['3.4.5', '3.4.5'], expected: true }, + { values: ['3.4.5', '3.4.4'], expected: true }, + { values: ['3.4.5', '3.4.6'], expected: false }, + { values: ['3.4', '3.4.0'], expected: true }, + { values: ['3', '3.0.0'], expected: true }, + { values: ['3.1.1-beta1', '3.1'], expected: true }, + { values: ['3.4.5', '4'], expected: false }, + { values: ['3.4.5', '3.5'], expected: false }, ]; - cases.forEach((testCase) => { + cases.forEach(testCase => { expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected); }); }); diff --git a/public/test/index.ts b/public/test/index.ts index 33f24331b67..05a47686775 100644 --- a/public/test/index.ts +++ b/public/test/index.ts @@ -22,10 +22,6 @@ angular.module('grafana.filters', []); angular.module('grafana.routes', ['ngRoute']); const context = (require).context('../', true, /specs\.(tsx?|js)/); -for (let key of context.keys()) { +for (const key of context.keys()) { context(key); } - - - - diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 1531f2ed176..64d12fdf725 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -7,10 +7,10 @@ export const backendSrv = { }; export function createNavTree(...args) { - let root = []; + const root = []; let node = root; - for (let arg of args) { - let child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] }; + for (const arg of args) { + const child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] }; node.push(child); node = child.children; } From 314b645857bf82f6fef66e9d4c1af18dd6854567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 26 Aug 2018 18:43:07 +0200 Subject: [PATCH 408/410] tslint: changing vars -> const (#13034) --- .../alerting/specs/threshold_mapper.test.ts | 12 +- .../dashboard/specs/dashboard_model.test.ts | 48 +++---- .../features/dashboard/specs/exporter.test.ts | 30 ++--- .../dashboard/specs/save_as_modal.test.ts | 12 +- .../specs/save_provisioned_modal.test.ts | 6 +- .../dashboard/specs/share_modal_ctrl.test.ts | 10 +- .../features/dashboard/specs/time_srv.test.ts | 22 ++-- .../dashboard/specs/viewstate_srv.test.ts | 2 +- .../panellinks/specs/link_srv.test.ts | 18 +-- .../templating/specs/adhoc_variable.test.ts | 6 +- .../templating/specs/query_variable.test.ts | 14 +-- .../templating/specs/template_srv.test.ts | 100 +++++++-------- .../templating/specs/variable.test.ts | 22 ++-- .../templating/specs/variable_srv.test.ts | 26 ++-- .../specs/variable_srv_init.test.ts | 16 +-- .../cloudwatch/specs/datasource.test.ts | 32 ++--- .../elasticsearch/specs/datasource.test.ts | 24 ++-- .../elasticsearch/specs/index_pattern.test.ts | 18 +-- .../elasticsearch/specs/query_builder.test.ts | 52 ++++---- .../elasticsearch/specs/query_def.test.ts | 18 +-- .../datasource/grafana-live/datasource.ts | 6 +- .../graphite/specs/datasource.test.ts | 2 +- .../datasource/graphite/specs/gfunc.test.ts | 42 +++---- .../datasource/graphite/specs/lexer.test.ts | 56 ++++----- .../datasource/graphite/specs/parser.test.ts | 80 ++++++------ .../influxdb/specs/influx_query.test.ts | 66 +++++----- .../influxdb/specs/influx_series.test.ts | 64 +++++----- .../influxdb/specs/query_builder.test.ts | 68 +++++----- .../influxdb/specs/query_part.test.ts | 32 ++--- .../influxdb/specs/response_parser.test.ts | 32 ++--- .../opentsdb/specs/datasource.test.ts | 4 +- .../opentsdb/specs/query_ctrl.test.ts | 2 +- .../prometheus/specs/datasource.test.ts | 118 +++++++++--------- .../specs/result_transformer.test.ts | 12 +- .../panel/graph/specs/data_processor.test.ts | 10 +- .../plugins/panel/graph/specs/graph.test.ts | 16 +-- .../panel/graph/specs/graph_ctrl.test.ts | 8 +- .../panel/graph/specs/graph_tooltip.test.ts | 24 ++-- .../graph/specs/threshold_manager.test.ts | 28 ++--- .../panel/heatmap/specs/heatmap_ctrl.test.ts | 8 +- .../singlestat/specs/singlestat_panel.test.ts | 6 +- .../panel/table/specs/renderer.test.ts | 76 +++++------ .../panel/table/specs/transformers.test.ts | 40 +++--- public/test/jest-setup.ts | 2 +- public/test/specs/helpers.ts | 10 +- 45 files changed, 650 insertions(+), 650 deletions(-) diff --git a/public/app/features/alerting/specs/threshold_mapper.test.ts b/public/app/features/alerting/specs/threshold_mapper.test.ts index b9fa45a6e49..922d9c8787e 100644 --- a/public/app/features/alerting/specs/threshold_mapper.test.ts +++ b/public/app/features/alerting/specs/threshold_mapper.test.ts @@ -5,7 +5,7 @@ import { ThresholdMapper } from '../threshold_mapper'; describe('ThresholdMapper', () => { describe('with greater than evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -17,7 +17,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('gt'); expect(panel.thresholds[0].value).toBe(100); @@ -26,7 +26,7 @@ describe('ThresholdMapper', () => { describe('with outside range evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -38,7 +38,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('lt'); expect(panel.thresholds[0].value).toBe(100); @@ -50,7 +50,7 @@ describe('ThresholdMapper', () => { describe('with inside range evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -62,7 +62,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('gt'); expect(panel.thresholds[0].value).toBe(100); diff --git a/public/app/features/dashboard/specs/dashboard_model.test.ts b/public/app/features/dashboard/specs/dashboard_model.test.ts index 28029653a6c..24d036a8233 100644 --- a/public/app/features/dashboard/specs/dashboard_model.test.ts +++ b/public/app/features/dashboard/specs/dashboard_model.test.ts @@ -6,7 +6,7 @@ jest.mock('app/core/services/context_srv', () => ({})); describe('DashboardModel', function() { describe('when creating new dashboard model defaults only', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({}, {}); @@ -27,7 +27,7 @@ describe('DashboardModel', function() { }); describe('when getting next panel id', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -42,16 +42,16 @@ describe('DashboardModel', function() { describe('getSaveModelClone', function() { it('should sort keys', () => { - var model = new DashboardModel({}); - var saveModel = model.getSaveModelClone(); - var keys = _.keys(saveModel); + const model = new DashboardModel({}); + const saveModel = model.getSaveModelClone(); + const keys = _.keys(saveModel); expect(keys[0]).toBe('annotations'); expect(keys[1]).toBe('autoUpdate'); }); it('should remove add panel panels', () => { - var model = new DashboardModel({}); + const model = new DashboardModel({}); model.addPanel({ type: 'add-panel', }); @@ -61,15 +61,15 @@ describe('DashboardModel', function() { model.addPanel({ type: 'add-panel', }); - var saveModel = model.getSaveModelClone(); - var panels = saveModel.panels; + const saveModel = model.getSaveModelClone(); + const panels = saveModel.panels; expect(panels.length).toBe(1); }); }); describe('row and panel manipulation', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({}); @@ -82,7 +82,7 @@ describe('DashboardModel', function() { }); it('duplicate panel should try to add to the right if there is space', function() { - var panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 } }; + const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 } }; dashboard.addPanel(panel); dashboard.duplicatePanel(dashboard.panels[0]); @@ -96,7 +96,7 @@ describe('DashboardModel', function() { }); it('duplicate panel should remove repeat data', function() { - var panel = { + const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 }, repeat: 'asd', @@ -112,7 +112,7 @@ describe('DashboardModel', function() { }); describe('Given editable false dashboard', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ editable: false }); @@ -124,14 +124,14 @@ describe('DashboardModel', function() { }); it('getSaveModelClone should remove meta', function() { - var clone = model.getSaveModelClone(); + const clone = model.getSaveModelClone(); expect(clone.meta).toBe(undefined); }); }); describe('when loading dashboard with old influxdb query schema', function() { - var model; - var target; + let model; + let target; beforeEach(function() { model = new DashboardModel({ @@ -197,7 +197,7 @@ describe('DashboardModel', function() { }); describe('when creating dashboard model with missing list for annoations or templating', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -222,7 +222,7 @@ describe('DashboardModel', function() { }); describe('Formatting epoch timestamp when timezone is set as utc', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({ timezone: 'utc' }); @@ -242,7 +242,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with empty lists', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({}); @@ -255,7 +255,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with annotation', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -272,7 +272,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with template var', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -289,7 +289,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with hidden template var', function() { - var model; + let model; beforeEach(function() { model = new DashboardModel({ @@ -306,7 +306,7 @@ describe('DashboardModel', function() { }); describe('updateSubmenuVisibility with hidden annotation toggle', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({ @@ -323,7 +323,7 @@ describe('DashboardModel', function() { }); describe('When collapsing row', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({ @@ -365,7 +365,7 @@ describe('DashboardModel', function() { }); describe('When expanding row', function() { - var dashboard; + let dashboard; beforeEach(function() { dashboard = new DashboardModel({ diff --git a/public/app/features/dashboard/specs/exporter.test.ts b/public/app/features/dashboard/specs/exporter.test.ts index c7727a4af4d..c7a232f925b 100644 --- a/public/app/features/dashboard/specs/exporter.test.ts +++ b/public/app/features/dashboard/specs/exporter.test.ts @@ -10,7 +10,7 @@ import { DashboardExporter } from '../export/exporter'; import { DashboardModel } from '../dashboard_model'; describe('given dashboard with repeated panels', () => { - var dash, exported; + let dash, exported; beforeEach(done => { dash = { @@ -89,7 +89,7 @@ describe('given dashboard with repeated panels', () => { config.buildInfo.version = '3.0.2'; //Stubs test function calls - var datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) }; + const datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) }; config.panels['graph'] = { id: 'graph', @@ -110,7 +110,7 @@ describe('given dashboard with repeated panels', () => { }; dash = new DashboardModel(dash, {}); - var exporter = new DashboardExporter(datasourceSrvStub); + const exporter = new DashboardExporter(datasourceSrvStub); exporter.makeExportable(dash).then(clean => { exported = clean; done(); @@ -118,12 +118,12 @@ describe('given dashboard with repeated panels', () => { }); it('should replace datasource refs', () => { - var panel = exported.panels[0]; + const panel = exported.panels[0]; expect(panel.datasource).toBe('${DS_GFDB}'); }); it('should replace datasource refs in collapsed row', () => { - var panel = exported.panels[5].panels[0]; + const panel = exported.panels[5].panels[0]; expect(panel.datasource).toBe('${DS_GFDB}'); }); @@ -145,7 +145,7 @@ describe('given dashboard with repeated panels', () => { }); it('should add datasource to required', () => { - var require = _.find(exported.__requires, { name: 'TestDB' }); + const require = _.find(exported.__requires, { name: 'TestDB' }); expect(require.name).toBe('TestDB'); expect(require.id).toBe('testdb'); expect(require.type).toBe('datasource'); @@ -153,52 +153,52 @@ describe('given dashboard with repeated panels', () => { }); it('should not add built in datasources to required', () => { - var require = _.find(exported.__requires, { name: 'Mixed' }); + const require = _.find(exported.__requires, { name: 'Mixed' }); expect(require).toBe(undefined); }); it('should add datasources used in mixed mode', () => { - var require = _.find(exported.__requires, { name: 'OtherDB' }); + const require = _.find(exported.__requires, { name: 'OtherDB' }); expect(require).not.toBe(undefined); }); it('should add graph panel to required', () => { - var require = _.find(exported.__requires, { name: 'Graph' }); + const require = _.find(exported.__requires, { name: 'Graph' }); expect(require.name).toBe('Graph'); expect(require.id).toBe('graph'); expect(require.version).toBe('1.1.0'); }); it('should add table panel to required', () => { - var require = _.find(exported.__requires, { name: 'Table' }); + const require = _.find(exported.__requires, { name: 'Table' }); expect(require.name).toBe('Table'); expect(require.id).toBe('table'); expect(require.version).toBe('1.1.1'); }); it('should add heatmap panel to required', () => { - var require = _.find(exported.__requires, { name: 'Heatmap' }); + const require = _.find(exported.__requires, { name: 'Heatmap' }); expect(require.name).toBe('Heatmap'); expect(require.id).toBe('heatmap'); expect(require.version).toBe('1.1.2'); }); it('should add grafana version', () => { - var require = _.find(exported.__requires, { name: 'Grafana' }); + const require = _.find(exported.__requires, { name: 'Grafana' }); expect(require.type).toBe('grafana'); expect(require.id).toBe('grafana'); expect(require.version).toBe('3.0.2'); }); it('should add constant template variables as inputs', () => { - var input = _.find(exported.__inputs, { name: 'VAR_PREFIX' }); + const input = _.find(exported.__inputs, { name: 'VAR_PREFIX' }); expect(input.type).toBe('constant'); expect(input.label).toBe('prefix'); expect(input.value).toBe('collectd'); }); it('should templatize constant variables', () => { - var variable = _.find(exported.templating.list, { name: 'prefix' }); + const variable = _.find(exported.templating.list, { name: 'prefix' }); expect(variable.query).toBe('${VAR_PREFIX}'); expect(variable.current.text).toBe('${VAR_PREFIX}'); expect(variable.current.value).toBe('${VAR_PREFIX}'); @@ -208,7 +208,7 @@ describe('given dashboard with repeated panels', () => { }); // Stub responses -var stubs = []; +const stubs = []; stubs['gfdb'] = { name: 'gfdb', meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' }, diff --git a/public/app/features/dashboard/specs/save_as_modal.test.ts b/public/app/features/dashboard/specs/save_as_modal.test.ts index bb16d1bcc1c..29ed694474b 100644 --- a/public/app/features/dashboard/specs/save_as_modal.test.ts +++ b/public/app/features/dashboard/specs/save_as_modal.test.ts @@ -4,12 +4,12 @@ import { describe, it, expect } from 'test/lib/common'; describe('saving dashboard as', () => { function scenario(name, panel, verify) { describe(name, () => { - var json = { + const json = { title: 'name', panels: [panel], }; - var mockDashboardSrv = { + const mockDashboardSrv = { getCurrent: function() { return { id: 5, @@ -21,8 +21,8 @@ describe('saving dashboard as', () => { }, }; - var ctrl = new SaveDashboardAsModalCtrl(mockDashboardSrv); - var ctx: any = { + const ctrl = new SaveDashboardAsModalCtrl(mockDashboardSrv); + const ctx: any = { clone: ctrl.clone, ctrl: ctrl, panel: panel, @@ -35,14 +35,14 @@ describe('saving dashboard as', () => { } scenario('default values', {}, ctx => { - var clone = ctx.clone; + const clone = ctx.clone; expect(clone.id).toBe(null); expect(clone.title).toBe('name Copy'); expect(clone.editable).toBe(true); expect(clone.hideControls).toBe(false); }); - var graphPanel = { + const graphPanel = { id: 1, type: 'graph', alert: { rule: 1 }, diff --git a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts index ce921cee8c8..fb1a652a03c 100644 --- a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts +++ b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts @@ -1,12 +1,12 @@ import { SaveProvisionedDashboardModalCtrl } from '../save_provisioned_modal'; describe('SaveProvisionedDashboardModalCtrl', () => { - var json = { + const json = { title: 'name', id: 5, }; - var mockDashboardSrv = { + const mockDashboardSrv = { getCurrent: function() { return { id: 5, @@ -18,7 +18,7 @@ describe('SaveProvisionedDashboardModalCtrl', () => { }, }; - var ctrl = new SaveProvisionedDashboardModalCtrl(mockDashboardSrv); + const ctrl = new SaveProvisionedDashboardModalCtrl(mockDashboardSrv); it('should remove id from dashboard model', () => { expect(ctrl.dash.id).toBeUndefined(); diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts index 35261256566..796baf7f522 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts @@ -4,7 +4,7 @@ import config from 'app/core/config'; import { LinkSrv } from 'app/features/panellinks/link_srv'; describe('ShareModalCtrl', () => { - var ctx = { + const ctx = { timeSrv: { timeRange: () => { return { from: new Date(1000), to: new Date(2000) }; @@ -68,8 +68,8 @@ describe('ShareModalCtrl', () => { 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'; + const base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash'; + const params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; expect(ctx.scope.imageUrl).toContain(base + params); }); @@ -79,8 +79,8 @@ describe('ShareModalCtrl', () => { 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'; + const base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js'; + const params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; expect(ctx.scope.imageUrl).toContain(base + params); }); diff --git a/public/app/features/dashboard/specs/time_srv.test.ts b/public/app/features/dashboard/specs/time_srv.test.ts index f8d9e42cfd4..046ac52c9bf 100644 --- a/public/app/features/dashboard/specs/time_srv.test.ts +++ b/public/app/features/dashboard/specs/time_srv.test.ts @@ -3,25 +3,25 @@ import '../time_srv'; import moment from 'moment'; describe('timeSrv', function() { - var rootScope = { + const rootScope = { $on: jest.fn(), onAppEvent: jest.fn(), appEvent: jest.fn(), }; - var timer = { + const timer = { register: jest.fn(), cancel: jest.fn(), cancelAll: jest.fn(), }; - var location = { + let location = { search: jest.fn(() => ({})), }; - var timeSrv; + let timeSrv; - var _dashboard: any = { + const _dashboard: any = { time: { from: 'now-6h', to: 'now' }, getTimezone: jest.fn(() => 'browser'), }; @@ -34,14 +34,14 @@ describe('timeSrv', function() { describe('timeRange', function() { it('should return unparsed when parse is false', function() { timeSrv.setTime({ from: 'now', to: 'now-1h' }); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.raw.from).toBe('now'); expect(time.raw.to).toBe('now-1h'); }); it('should return parsed when parse is true', function() { timeSrv.setTime({ from: 'now', to: 'now-1h' }); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(moment.isMoment(time.from)).toBe(true); expect(moment.isMoment(time.to)).toBe(true); }); @@ -58,7 +58,7 @@ describe('timeSrv', function() { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.raw.from).toBe('now-2d'); expect(time.raw.to).toBe('now'); }); @@ -74,7 +74,7 @@ describe('timeSrv', function() { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.from.valueOf()).toEqual(new Date('2014-04-10T05:20:10Z').getTime()); expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime()); }); @@ -90,7 +90,7 @@ describe('timeSrv', function() { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.from.valueOf()).toEqual(new Date('2014-04-10T00:00:00Z').getTime()); expect(time.to.valueOf()).toEqual(new Date('2014-05-20T00:00:00Z').getTime()); }); @@ -106,7 +106,7 @@ describe('timeSrv', function() { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); - var time = timeSrv.timeRange(); + const time = timeSrv.timeRange(); expect(time.from.valueOf()).toEqual(1410337646373); expect(time.to.valueOf()).toEqual(1410337665699); }); diff --git a/public/app/features/dashboard/specs/viewstate_srv.test.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts index 740e3c3b9a8..905ffb8b355 100644 --- a/public/app/features/dashboard/specs/viewstate_srv.test.ts +++ b/public/app/features/dashboard/specs/viewstate_srv.test.ts @@ -37,7 +37,7 @@ describe('when updating view state', () => { }); it('should update querystring and view state', () => { - var updateState = { fullscreen: true, edit: true, panelId: 1 }; + const updateState = { fullscreen: true, edit: true, panelId: 1 }; viewState.update(updateState); diff --git a/public/app/features/panellinks/specs/link_srv.test.ts b/public/app/features/panellinks/specs/link_srv.test.ts index 521a4edef15..9c6b62d4b69 100644 --- a/public/app/features/panellinks/specs/link_srv.test.ts +++ b/public/app/features/panellinks/specs/link_srv.test.ts @@ -7,9 +7,9 @@ jest.mock('angular', () => { }); describe('linkSrv', function() { - var linkSrv; - var templateSrvMock = {}; - var timeSrvMock = {}; + let linkSrv; + const templateSrvMock = {}; + const timeSrvMock = {}; beforeEach(() => { linkSrv = new LinkSrv(templateSrvMock, timeSrvMock); @@ -17,29 +17,29 @@ describe('linkSrv', function() { describe('when appending query strings', function() { it('add ? to URL if not present', function() { - var url = linkSrv.appendToQueryString('http://example.com', 'foo=bar'); + const url = linkSrv.appendToQueryString('http://example.com', 'foo=bar'); expect(url).toBe('http://example.com?foo=bar'); }); it('do not add & to URL if ? is present but query string is empty', function() { - var url = linkSrv.appendToQueryString('http://example.com?', 'foo=bar'); + const url = linkSrv.appendToQueryString('http://example.com?', 'foo=bar'); expect(url).toBe('http://example.com?foo=bar'); }); it('add & to URL if query string is present', function() { - var url = linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world'); + const url = linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world'); expect(url).toBe('http://example.com?foo=bar&hello=world'); }); it('do not change the URL if there is nothing to append', function() { _.each(['', undefined, null], function(toAppend) { - var url1 = linkSrv.appendToQueryString('http://example.com', toAppend); + const url1 = linkSrv.appendToQueryString('http://example.com', toAppend); expect(url1).toBe('http://example.com'); - var url2 = linkSrv.appendToQueryString('http://example.com?', toAppend); + const url2 = linkSrv.appendToQueryString('http://example.com?', toAppend); expect(url2).toBe('http://example.com?'); - var url3 = linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend); + const url3 = linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend); expect(url3).toBe('http://example.com?foo=bar'); }); }); diff --git a/public/app/features/templating/specs/adhoc_variable.test.ts b/public/app/features/templating/specs/adhoc_variable.test.ts index a7b20e8d029..f85c49e73d5 100644 --- a/public/app/features/templating/specs/adhoc_variable.test.ts +++ b/public/app/features/templating/specs/adhoc_variable.test.ts @@ -3,21 +3,21 @@ import { AdhocVariable } from '../adhoc_variable'; describe('AdhocVariable', function() { describe('when serializing to url', function() { it('should set return key value and op separated by pipe', function() { - var variable = new AdhocVariable({ + const variable = new AdhocVariable({ filters: [ { key: 'key1', operator: '=', value: 'value1' }, { key: 'key2', operator: '!=', value: 'value2' }, { key: 'key3', operator: '=', value: 'value3a|value3b|value3c' }, ], }); - var urlValue = variable.getValueForUrl(); + const urlValue = variable.getValueForUrl(); expect(urlValue).toMatchObject(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']); }); }); describe('when deserializing from url', function() { it('should restore filters', function() { - var variable = new AdhocVariable({}); + const variable = new AdhocVariable({}); variable.setValueFromUrl(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']); expect(variable.filters[0].key).toBe('key1'); diff --git a/public/app/features/templating/specs/query_variable.test.ts b/public/app/features/templating/specs/query_variable.test.ts index 39c51874586..85a36702d3c 100644 --- a/public/app/features/templating/specs/query_variable.test.ts +++ b/public/app/features/templating/specs/query_variable.test.ts @@ -3,7 +3,7 @@ import { QueryVariable } from '../query_variable'; describe('QueryVariable', () => { describe('when creating from model', () => { it('should set defaults', () => { - var variable = new QueryVariable({}, null, null, null, null); + const variable = new QueryVariable({}, null, null, null, null); expect(variable.datasource).toBe(null); expect(variable.refresh).toBe(0); expect(variable.sort).toBe(0); @@ -15,13 +15,13 @@ describe('QueryVariable', () => { }); it('get model should copy changes back to model', () => { - var variable = new QueryVariable({}, null, null, null, null); + const variable = new QueryVariable({}, null, null, null, null); variable.options = [{ text: 'test' }]; variable.datasource = 'google'; variable.regex = 'asd'; variable.sort = 50; - var model = variable.getSaveModel(); + const model = variable.getSaveModel(); expect(model.options.length).toBe(1); expect(model.options[0].text).toBe('test'); expect(model.datasource).toBe('google'); @@ -30,11 +30,11 @@ describe('QueryVariable', () => { }); it('if refresh != 0 then remove options in presisted mode', () => { - var variable = new QueryVariable({}, null, null, null, null); + const variable = new QueryVariable({}, null, null, null, null); variable.options = [{ text: 'test' }]; variable.refresh = 1; - var model = variable.getSaveModel(); + const model = variable.getSaveModel(); expect(model.options.length).toBe(0); }); }); @@ -69,7 +69,7 @@ describe('QueryVariable', () => { }); it('should return in same order', () => { - var i = 0; + let i = 0; expect(result.length).toBe(11); expect(result[i++].text).toBe(''); expect(result[i++].text).toBe('0'); @@ -90,7 +90,7 @@ describe('QueryVariable', () => { }); it('should return in same order', () => { - var i = 0; + let i = 0; expect(result.length).toBe(11); expect(result[i++].text).toBe(''); expect(result[i++].text).toBe('0'); diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts index 86b6aa7ec99..984d62cb729 100644 --- a/public/app/features/templating/specs/template_srv.test.ts +++ b/public/app/features/templating/specs/template_srv.test.ts @@ -1,7 +1,7 @@ import { TemplateSrv } from '../template_srv'; describe('templateSrv', function() { - var _templateSrv; + let _templateSrv; function initTemplateSrv(variables) { _templateSrv = new TemplateSrv(); @@ -14,7 +14,7 @@ describe('templateSrv', function() { }); it('should initialize template data', function() { - var target = _templateSrv.replace('this.[[test]].filters'); + const target = _templateSrv.replace('this.[[test]].filters'); expect(target).toBe('this.oogle.filters'); }); }); @@ -25,42 +25,42 @@ describe('templateSrv', function() { }); it('should replace $test with scoped value', function() { - var target = _templateSrv.replace('this.$test.filters', { + const target = _templateSrv.replace('this.$test.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); it('should replace ${test} with scoped value', function() { - var target = _templateSrv.replace('this.${test}.filters', { + const target = _templateSrv.replace('this.${test}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); it('should replace ${test:glob} with scoped value', function() { - var target = _templateSrv.replace('this.${test:glob}.filters', { + const target = _templateSrv.replace('this.${test:glob}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.mupp.filters'); }); it('should replace $test with scoped text', function() { - var target = _templateSrv.replaceWithText('this.$test.filters', { + const target = _templateSrv.replaceWithText('this.$test.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); }); it('should replace ${test} with scoped text', function() { - var target = _templateSrv.replaceWithText('this.${test}.filters', { + const target = _templateSrv.replaceWithText('this.${test}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); }); it('should replace ${test:glob} with scoped text', function() { - var target = _templateSrv.replaceWithText('this.${test:glob}.filters', { + const target = _templateSrv.replaceWithText('this.${test:glob}.filters', { test: { value: 'mupp', text: 'asd' }, }); expect(target).toBe('this.asd.filters'); @@ -81,17 +81,17 @@ describe('templateSrv', function() { }); it('should return filters if datasourceName match', function() { - var filters = _templateSrv.getAdhocFilters('oogle'); + const filters = _templateSrv.getAdhocFilters('oogle'); expect(filters).toMatchObject([1]); }); it('should return empty array if datasourceName does not match', function() { - var filters = _templateSrv.getAdhocFilters('oogleasdasd'); + const filters = _templateSrv.getAdhocFilters('oogleasdasd'); expect(filters).toMatchObject([]); }); it('should return filters when datasourceName match via data source variable', function() { - var filters = _templateSrv.getAdhocFilters('logstash'); + const filters = _templateSrv.getAdhocFilters('logstash'); expect(filters).toMatchObject([2]); }); }); @@ -108,37 +108,37 @@ describe('templateSrv', function() { }); it('should replace $test with globbed value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test} with globbed value', function() { - var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test:glob} with globbed value', function() { - var target = _templateSrv.replace('this.${test:glob}.filters', {}); + const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace $test with piped value', function() { - var target = _templateSrv.replace('this=$test', {}, 'pipe'); + const target = _templateSrv.replace('this=$test', {}, 'pipe'); expect(target).toBe('this=value1|value2'); }); it('should replace ${test} with piped value', function() { - var target = _templateSrv.replace('this=${test}', {}, 'pipe'); + const target = _templateSrv.replace('this=${test}', {}, 'pipe'); expect(target).toBe('this=value1|value2'); }); it('should replace ${test:pipe} with piped value', function() { - var target = _templateSrv.replace('this=${test:pipe}', {}); + const target = _templateSrv.replace('this=${test:pipe}', {}); expect(target).toBe('this=value1|value2'); }); it('should replace ${test:pipe} with piped value and $test with globbed value', function() { - var target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); + const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); expect(target).toBe('value1|value2,{value1,value2}'); }); }); @@ -156,22 +156,22 @@ describe('templateSrv', function() { }); it('should replace $test with formatted all value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test} with formatted all value', function() { - var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test:glob} with formatted all value', function() { - var target = _templateSrv.replace('this.${test:glob}.filters', {}); + const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.{value1,value2}.filters'); }); it('should replace ${test:pipe} with piped value and $test with globbed value', function() { - var target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); + const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); expect(target).toBe('value1|value2,{value1,value2}'); }); }); @@ -190,22 +190,22 @@ describe('templateSrv', function() { }); it('should replace $test with formatted all value', function() { - var target = _templateSrv.replace('this.$test.filters', {}, 'glob'); + const target = _templateSrv.replace('this.$test.filters', {}, 'glob'); expect(target).toBe('this.*.filters'); }); it('should replace ${test} with formatted all value', function() { - var target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); + const target = _templateSrv.replace('this.${test}.filters', {}, 'glob'); expect(target).toBe('this.*.filters'); }); it('should replace ${test:glob} with formatted all value', function() { - var target = _templateSrv.replace('this.${test:glob}.filters', {}); + const target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.*.filters'); }); it('should not escape custom all value', function() { - var target = _templateSrv.replace('this.$test', {}, 'regex'); + const target = _templateSrv.replace('this.$test', {}, 'regex'); expect(target).toBe('this.*'); }); }); @@ -213,70 +213,70 @@ describe('templateSrv', function() { describe('lucene format', function() { it('should properly escape $test with lucene escape sequences', function() { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); - var target = _templateSrv.replace('this:$test', {}, 'lucene'); + const target = _templateSrv.replace('this:$test', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); it('should properly escape ${test} with lucene escape sequences', function() { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); - var target = _templateSrv.replace('this:${test}', {}, 'lucene'); + const target = _templateSrv.replace('this:${test}', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); it('should properly escape ${test:lucene} with lucene escape sequences', function() { initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); - var target = _templateSrv.replace('this:${test:lucene}', {}); + const target = _templateSrv.replace('this:${test:lucene}', {}); expect(target).toBe('this:value\\/4'); }); }); describe('format variable to string values', function() { it('single value should return value', function() { - var result = _templateSrv.formatValue('test'); + const result = _templateSrv.formatValue('test'); expect(result).toBe('test'); }); it('multi value and glob format should render glob string', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'glob'); + const result = _templateSrv.formatValue(['test', 'test2'], 'glob'); expect(result).toBe('{test,test2}'); }); it('multi value and lucene should render as lucene expr', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'lucene'); + const result = _templateSrv.formatValue(['test', 'test2'], 'lucene'); expect(result).toBe('("test" OR "test2")'); }); it('multi value and regex format should render regex string', function() { - var result = _templateSrv.formatValue(['test.', 'test2'], 'regex'); + const result = _templateSrv.formatValue(['test.', 'test2'], 'regex'); expect(result).toBe('(test\\.|test2)'); }); it('multi value and pipe should render pipe string', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'pipe'); + const result = _templateSrv.formatValue(['test', 'test2'], 'pipe'); expect(result).toBe('test|test2'); }); it('multi value and distributed should render distributed string', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'distributed', { + const result = _templateSrv.formatValue(['test', 'test2'], 'distributed', { name: 'build', }); expect(result).toBe('test,build=test2'); }); it('multi value and distributed should render when not string', function() { - var result = _templateSrv.formatValue(['test'], 'distributed', { + const result = _templateSrv.formatValue(['test'], 'distributed', { name: 'build', }); expect(result).toBe('test'); }); it('multi value and csv format should render csv string', function() { - var result = _templateSrv.formatValue(['test', 'test2'], 'csv'); + const result = _templateSrv.formatValue(['test', 'test2'], 'csv'); expect(result).toBe('test,test2'); }); it('slash should be properly escaped in regex format', function() { - var result = _templateSrv.formatValue('Gi3/14', 'regex'); + const result = _templateSrv.formatValue('Gi3/14', 'regex'); expect(result).toBe('Gi3\\/14'); }); }); @@ -287,7 +287,7 @@ describe('templateSrv', function() { }); it('should return true if exists', function() { - var result = _templateSrv.variableExists('$test'); + const result = _templateSrv.variableExists('$test'); expect(result).toBe(true); }); }); @@ -298,17 +298,17 @@ describe('templateSrv', function() { }); it('should insert html', function() { - var result = _templateSrv.highlightVariablesAsHtml('$test'); + const result = _templateSrv.highlightVariablesAsHtml('$test'); expect(result).toBe('$test'); }); it('should insert html anywhere in string', function() { - var result = _templateSrv.highlightVariablesAsHtml('this $test ok'); + const result = _templateSrv.highlightVariablesAsHtml('this $test ok'); expect(result).toBe('this $test ok'); }); it('should ignore if variables does not exist', function() { - var result = _templateSrv.highlightVariablesAsHtml('this $google ok'); + const result = _templateSrv.highlightVariablesAsHtml('this $google ok'); expect(result).toBe('this $google ok'); }); }); @@ -319,7 +319,7 @@ describe('templateSrv', function() { }); it('should set current value and update template data', function() { - var target = _templateSrv.replace('this.[[test]].filters'); + const target = _templateSrv.replace('this.[[test]].filters'); expect(target).toBe('this.muuuu.filters'); }); }); @@ -339,7 +339,7 @@ describe('templateSrv', function() { }); it('should set multiple url params', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toMatchObject(['val1', 'val2']); }); @@ -360,7 +360,7 @@ describe('templateSrv', function() { }); it('should not include template variable value in url', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toBe(undefined); }); @@ -382,7 +382,7 @@ describe('templateSrv', function() { }); it('should not include template variable value in url', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params); expect(params['var-test']).toBe(undefined); }); @@ -394,7 +394,7 @@ describe('templateSrv', function() { }); it('should set scoped value as url params', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params, { test: { value: 'val1' }, }); @@ -408,7 +408,7 @@ describe('templateSrv', function() { }); it('should not set scoped value as url params', function() { - var params = {}; + const params = {}; _templateSrv.fillVariableValuesForUrl(params, { test: { name: 'test', value: 'val1', skipUrlSync: true }, }); @@ -435,7 +435,7 @@ describe('templateSrv', function() { }); it('should replace with text except for grafanaVariables', function() { - var target = _templateSrv.replaceWithText('Server: $server, period: $period'); + const target = _templateSrv.replaceWithText('Server: $server, period: $period'); expect(target).toBe('Server: All, period: 13m'); }); }); @@ -446,7 +446,7 @@ describe('templateSrv', function() { }); it('should replace $__interval_ms with interval milliseconds', function() { - var target = _templateSrv.replace('10 * $__interval_ms', { + const target = _templateSrv.replace('10 * $__interval_ms', { __interval_ms: { text: '100', value: '100' }, }); expect(target).toBe('10 * 100'); diff --git a/public/app/features/templating/specs/variable.test.ts b/public/app/features/templating/specs/variable.test.ts index cfe084957ec..814c5fbe003 100644 --- a/public/app/features/templating/specs/variable.test.ts +++ b/public/app/features/templating/specs/variable.test.ts @@ -2,38 +2,38 @@ import { containsVariable, assignModelProperties } from '../variable'; describe('containsVariable', function() { describe('when checking if a string contains a variable', function() { - it('should find it with $var syntax', function() { - var contains = containsVariable('this.$test.filters', 'test'); + it('should find it with $const syntax', function() { + const contains = containsVariable('this.$test.filters', 'test'); expect(contains).toBe(true); }); - it('should not find it if only part matches with $var syntax', function() { - var contains = containsVariable('this.$serverDomain.filters', 'server'); + it('should not find it if only part matches with $const syntax', function() { + const contains = containsVariable('this.$serverDomain.filters', 'server'); expect(contains).toBe(false); }); it('should find it if it ends with variable and passing multiple test strings', function() { - var contains = containsVariable('show field keys from $pgmetric', 'test string2', 'pgmetric'); + const contains = containsVariable('show field keys from $pgmetric', 'test string2', 'pgmetric'); expect(contains).toBe(true); }); it('should find it with [[var]] syntax', function() { - var contains = containsVariable('this.[[test]].filters', 'test'); + const contains = containsVariable('this.[[test]].filters', 'test'); expect(contains).toBe(true); }); it('should find it when part of segment', function() { - var contains = containsVariable('metrics.$env.$group-*', 'group'); + const contains = containsVariable('metrics.$env.$group-*', 'group'); expect(contains).toBe(true); }); it('should find it its the only thing', function() { - var contains = containsVariable('$env', 'env'); + const contains = containsVariable('$env', 'env'); expect(contains).toBe(true); }); it('should be able to pass in multiple test strings', function() { - var contains = containsVariable('asd', 'asd2.$env', 'env'); + const contains = containsVariable('asd', 'asd2.$env', 'env'); expect(contains).toBe(true); }); }); @@ -41,14 +41,14 @@ describe('containsVariable', function() { describe('assignModelProperties', function() { it('only set properties defined in defaults', function() { - var target: any = { test: 'asd' }; + const target: any = { test: 'asd' }; assignModelProperties(target, { propA: 1, propB: 2 }, { propB: 0 }); expect(target.propB).toBe(2); expect(target.test).toBe('asd'); }); it('use default value if not found on source', function() { - var target: any = { test: 'asd' }; + const target: any = { test: 'asd' }; assignModelProperties(target, { propA: 1, propB: 2 }, { propC: 10 }); expect(target.propC).toBe(10); }); diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index f7796434b5e..28fd3860ed3 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -4,7 +4,7 @@ import moment from 'moment'; import $q from 'q'; describe('VariableSrv', function() { - var ctx = { + const ctx = { datasourceSrv: {}, timeSrv: { timeRange: () => {}, @@ -33,7 +33,7 @@ describe('VariableSrv', function() { function describeUpdateVariable(desc, fn) { describe(desc, () => { - var scenario: any = {}; + const scenario: any = {}; scenario.setup = function(setupFn) { scenario.setupFn = setupFn; }; @@ -41,7 +41,7 @@ describe('VariableSrv', function() { beforeEach(async () => { scenario.setupFn(); - var ds: any = {}; + const ds: any = {}; ds.metricFindQuery = () => Promise.resolve(scenario.queryResult); ctx.variableSrv = new VariableSrv(ctx.$rootScope, $q, ctx.$location, ctx.$injector, ctx.templateSrv); @@ -100,7 +100,7 @@ describe('VariableSrv', function() { auto_count: 10, }; - var range = { + const range = { from: moment(new Date()) .subtract(7, 'days') .toDate(), @@ -118,7 +118,7 @@ describe('VariableSrv', function() { }); it('should set $__auto_interval_test', () => { - var call = ctx.templateSrv.setGrafanaVariable.mock.calls[0]; + const call = ctx.templateSrv.setGrafanaVariable.mock.calls[0]; expect(call[0]).toBe('$__auto_interval_test'); expect(call[1]).toBe('12h'); }); @@ -126,7 +126,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', () => { - var call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop(); + const call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop(); expect(call[0]).toBe('$__auto_interval'); expect(call[1]).toBe('12h'); }); @@ -503,10 +503,10 @@ describe('VariableSrv', function() { }); describe('multiple interval variables with auto', () => { - var variable1, variable2; + let variable1, variable2; beforeEach(() => { - var range = { + const range = { from: moment(new Date()) .subtract(7, 'days') .toDate(), @@ -515,7 +515,7 @@ describe('VariableSrv', function() { ctx.timeSrv.timeRange = () => range; ctx.templateSrv.setGrafanaVariable = jest.fn(); - var variableModel1 = { + const variableModel1 = { type: 'interval', query: '1s,2h,5h,1d', name: 'variable1', @@ -525,7 +525,7 @@ describe('VariableSrv', function() { variable1 = ctx.variableSrv.createVariableFromModel(variableModel1); ctx.variableSrv.addVariable(variable1); - var variableModel2 = { + const variableModel2 = { type: 'interval', query: '1s,2h,5h', name: 'variable2', @@ -550,14 +550,14 @@ describe('VariableSrv', function() { }); it('should correctly set $__auto_interval_variableX', () => { - var variable1Set, + let 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]; + for (let i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) { + const call = ctx.templateSrv.setGrafanaVariable.mock.calls[i]; switch (call[0]) { case '$__auto_interval_variable1': expect(call[1]).toBe('12h'); diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index e011d4d0d15..f06f533e429 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -26,7 +26,7 @@ describe('VariableSrv init', function() { function describeInitScenario(desc, fn) { describe(desc, () => { - var scenario: any = { + const scenario: any = { urlParams: {}, setup: setupFn => { scenario.setupFn = setupFn; @@ -92,7 +92,7 @@ describe('VariableSrv init', function() { }); describe('given dependent variables', () => { - var variableList = [ + const variableList = [ { name: 'app', type: 'query', @@ -110,7 +110,7 @@ describe('VariableSrv init', function() { }, ]; - describeInitScenario('when setting parent var from url', scenario => { + describeInitScenario('when setting parent const from url', scenario => { scenario.setup(() => { scenario.variables = _.cloneDeep(variableList); scenario.urlParams['var-app'] = 'google'; @@ -148,7 +148,7 @@ describe('VariableSrv init', function() { }); it('should update current value', () => { - var variable = ctx.variableSrv.variables[0]; + const variable = ctx.variableSrv.variables[0]; expect(variable.options.length).toBe(2); }); }); @@ -172,7 +172,7 @@ describe('VariableSrv init', function() { }); it('should update current value', () => { - var variable = ctx.variableSrv.variables[0]; + const 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'); @@ -182,7 +182,7 @@ describe('VariableSrv init', function() { }); it('should set options that are not in value to selected false', () => { - var variable = ctx.variableSrv.variables[0]; + const variable = ctx.variableSrv.variables[0]; expect(variable.options[2].selected).toBe(false); }); }); @@ -206,7 +206,7 @@ describe('VariableSrv init', function() { }); it('should update current value', () => { - var variable = ctx.variableSrv.variables[0]; + const 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'); @@ -216,7 +216,7 @@ describe('VariableSrv init', function() { }); it('should set options that are not in value to selected false', () => { - var variable = ctx.variableSrv.variables[0]; + const variable = ctx.variableSrv.variables[0]; expect(variable.options[2].selected).toBe(false); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index eae3e91d37d..08329ba4e73 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -35,9 +35,9 @@ describe('CloudWatchDatasource', function() { }); describe('When performing CloudWatch query', function() { - var requestParams; + let requestParams; - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -54,7 +54,7 @@ describe('CloudWatchDatasource', function() { ], }; - var response = { + const response = { timings: [null], results: { A: { @@ -82,7 +82,7 @@ describe('CloudWatchDatasource', function() { it('should generate the correct query', function(done) { ctx.ds.query(query).then(function() { - var params = requestParams.queries[0]; + const params = requestParams.queries[0]; expect(params.namespace).toBe(query.targets[0].namespace); expect(params.metricName).toBe(query.targets[0].metricName); expect(params.dimensions['InstanceId']).toBe('i-12345678'); @@ -97,7 +97,7 @@ describe('CloudWatchDatasource', function() { period: '10m', }; - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -115,14 +115,14 @@ describe('CloudWatchDatasource', function() { }; ctx.ds.query(query).then(function() { - var params = requestParams.queries[0]; + const params = requestParams.queries[0]; expect(params.period).toBe('600'); done(); }); }); it('should cancel query for invalid extended statistics', function() { - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -152,7 +152,7 @@ describe('CloudWatchDatasource', function() { describe('When query region is "default"', function() { it('should return the datasource region if empty or "default"', function() { - var defaultRegion = instanceSettings.jsonData.defaultRegion; + const defaultRegion = instanceSettings.jsonData.defaultRegion; expect(ctx.ds.getActualRegion()).toBe(defaultRegion); expect(ctx.ds.getActualRegion('')).toBe(defaultRegion); @@ -163,7 +163,7 @@ describe('CloudWatchDatasource', function() { expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1'); }); - var requestParams; + let requestParams; beforeEach(function() { ctx.ds.performTimeSeriesQuery = jest.fn(request => { requestParams = request; @@ -172,7 +172,7 @@ describe('CloudWatchDatasource', function() { }); it('should query for the datasource region if empty or "default"', function(done) { - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -197,7 +197,7 @@ describe('CloudWatchDatasource', function() { }); describe('When performing CloudWatch query for extended statistics', function() { - var query = { + const query = { range: { from: 'now-1h', to: 'now' }, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -215,7 +215,7 @@ describe('CloudWatchDatasource', function() { ], }; - var response = { + const response = { timings: [null], results: { A: { @@ -379,10 +379,10 @@ describe('CloudWatchDatasource', function() { }); it('should caclculate the correct period', function() { - var hourSec = 60 * 60; - var daySec = hourSec * 24; - var start = 1483196400 * 1000; - var testData: any[] = [ + const hourSec = 60 * 60; + const daySec = hourSec * 24; + const start = 1483196400 * 1000; + const testData: any[] = [ [ { period: 60, namespace: 'AWS/EC2' }, { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } }, diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts index d1e2e3ba835..d37d1d86d54 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts @@ -53,7 +53,7 @@ describe('ElasticDatasource', function() { }); it('should translate index pattern to current day', function() { - var requestOptions; + let requestOptions; ctx.backendSrv.datasourceRequest = jest.fn(options => { requestOptions = options; return Promise.resolve({ data: {} }); @@ -61,13 +61,13 @@ describe('ElasticDatasource', function() { ctx.ds.testDatasource(); - var today = moment.utc().format('YYYY.MM.DD'); + const today = moment.utc().format('YYYY.MM.DD'); expect(requestOptions.url).toBe('http://es.com/asd-' + today + '/_mapping'); }); }); describe('When issuing metric query with interval pattern', function() { - var requestOptions, parts, header; + let requestOptions, parts, header; beforeEach(() => { createDatasource({ @@ -104,13 +104,13 @@ describe('ElasticDatasource', function() { }); it('should json escape lucene query', function() { - var body = angular.fromJson(parts[1]); + const body = angular.fromJson(parts[1]); expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test'); }); }); describe('When issuing document query', function() { - var requestOptions, parts, header; + let requestOptions, parts, header; beforeEach(function() { createDatasource({ @@ -147,7 +147,7 @@ describe('ElasticDatasource', function() { }); it('should set size', function() { - var body = angular.fromJson(parts[1]); + const body = angular.fromJson(parts[1]); expect(body.size).toBe(500); }); }); @@ -210,7 +210,7 @@ describe('ElasticDatasource', function() { query: '*', }) .then(fieldObjects => { - var fields = _.map(fieldObjects, 'text'); + const fields = _.map(fieldObjects, 'text'); expect(fields).toEqual([ '@timestamp', 'beat.name.raw', @@ -232,7 +232,7 @@ describe('ElasticDatasource', function() { type: 'number', }) .then(fieldObjects => { - var fields = _.map(fieldObjects, 'text'); + const fields = _.map(fieldObjects, 'text'); expect(fields).toEqual(['system.cpu.system', 'system.cpu.user', 'system.process.cpu.total']); }); @@ -243,14 +243,14 @@ describe('ElasticDatasource', function() { type: 'date', }) .then(fieldObjects => { - var fields = _.map(fieldObjects, 'text'); + const fields = _.map(fieldObjects, 'text'); expect(fields).toEqual(['@timestamp']); }); }); }); describe('When issuing aggregation query on es5.x', function() { - var requestOptions, parts, header; + let requestOptions, parts, header; beforeEach(function() { createDatasource({ @@ -287,13 +287,13 @@ describe('ElasticDatasource', function() { }); it('should set size to 0', function() { - var body = angular.fromJson(parts[1]); + const body = angular.fromJson(parts[1]); expect(body.size).toBe(0); }); }); describe('When issuing metricFind query on es5.x', function() { - var requestOptions, parts, header, body, results; + let requestOptions, parts, header, body, results; beforeEach(() => { createDatasource({ diff --git a/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts b/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts index 2f921e10425..c5cf4c9dee0 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/index_pattern.test.ts @@ -6,8 +6,8 @@ import { IndexPattern } from '../index_pattern'; describe('IndexPattern', () => { describe('when getting index for today', () => { test('should return correct index name', () => { - var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); - var expected = 'asd-' + moment.utc().format('YYYY.MM.DD'); + const pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); + const expected = 'asd-' + moment.utc().format('YYYY.MM.DD'); expect(pattern.getIndexForToday()).toBe(expected); }); @@ -16,20 +16,20 @@ describe('IndexPattern', () => { describe('when getting index list for time range', () => { describe('no interval', () => { test('should return correct index', () => { - var pattern = new IndexPattern('my-metrics', null); - var from = new Date(2015, 4, 30, 1, 2, 3); - var to = new Date(2015, 5, 1, 12, 5, 6); + const pattern = new IndexPattern('my-metrics', null); + const from = new Date(2015, 4, 30, 1, 2, 3); + const to = new Date(2015, 5, 1, 12, 5, 6); expect(pattern.getIndexList(from, to)).toEqual('my-metrics'); }); }); describe('daily', () => { test('should return correct index list', () => { - var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); - var from = new Date(1432940523000); - var to = new Date(1433153106000); + const pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily'); + const from = new Date(1432940523000); + const to = new Date(1433153106000); - var expected = ['asd-2015.05.29', 'asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']; + const expected = ['asd-2015.05.29', 'asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01']; expect(pattern.getIndexList(from, to)).toEqual(expected); }); diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts index 1dde47915d9..e4c9404e667 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts @@ -1,14 +1,14 @@ import { ElasticQueryBuilder } from '../query_builder'; describe('ElasticQueryBuilder', () => { - var builder; + let builder; beforeEach(() => { builder = new ElasticQueryBuilder({ timeField: '@timestamp' }); }); it('with defaults', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'Count', id: '0' }], timeField: '@timestamp', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], @@ -19,12 +19,12 @@ describe('ElasticQueryBuilder', () => { }); it('with defaults on es5.x', () => { - var builder_5x = new ElasticQueryBuilder({ + const builder_5x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 5, }); - var query = builder_5x.build({ + const query = builder_5x.build({ metrics: [{ type: 'Count', id: '0' }], timeField: '@timestamp', bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }], @@ -35,7 +35,7 @@ describe('ElasticQueryBuilder', () => { }); it('with multiple bucket aggs', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'count', id: '1' }], timeField: '@timestamp', bucketAggs: [ @@ -49,7 +49,7 @@ describe('ElasticQueryBuilder', () => { }); it('with select field', () => { - var query = builder.build( + const query = builder.build( { metrics: [{ type: 'avg', field: '@value', id: '1' }], bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }], @@ -58,12 +58,12 @@ describe('ElasticQueryBuilder', () => { 1000 ); - var aggs = query.aggs['2'].aggs; + const aggs = query.aggs['2'].aggs; expect(aggs['1'].avg.field).toBe('@value'); }); it('with term agg and order by metric agg', () => { - var query = builder.build( + const query = builder.build( { metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }], bucketAggs: [ @@ -80,15 +80,15 @@ describe('ElasticQueryBuilder', () => { 1000 ); - var firstLevel = query.aggs['2']; - var secondLevel = firstLevel.aggs['3']; + const firstLevel = query.aggs['2']; + const secondLevel = firstLevel.aggs['3']; expect(firstLevel.aggs['5'].avg.field).toBe('@value'); expect(secondLevel.aggs['5'].avg.field).toBe('@value'); }); it('with metric percentiles', () => { - var query = builder.build( + const query = builder.build( { metrics: [ { @@ -106,14 +106,14 @@ describe('ElasticQueryBuilder', () => { 1000 ); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.aggs['1'].percentiles.field).toBe('@load_time'); expect(firstLevel.aggs['1'].percentiles.percents).toEqual([1, 2, 3, 4]); }); it('with filters aggs', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'count', id: '1' }], timeField: '@timestamp', bucketAggs: [ @@ -134,11 +134,11 @@ describe('ElasticQueryBuilder', () => { }); it('with filters aggs on es5.x', () => { - var builder_5x = new ElasticQueryBuilder({ + const builder_5x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 5, }); - var query = builder_5x.build({ + const query = builder_5x.build({ metrics: [{ type: 'count', id: '1' }], timeField: '@timestamp', bucketAggs: [ @@ -159,7 +159,7 @@ describe('ElasticQueryBuilder', () => { }); it('with raw_document metric', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'raw_document', id: '1', settings: {} }], timeField: '@timestamp', bucketAggs: [], @@ -168,7 +168,7 @@ describe('ElasticQueryBuilder', () => { expect(query.size).toBe(500); }); it('with raw_document metric size set', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ type: 'raw_document', id: '1', settings: { size: 1337 } }], timeField: '@timestamp', bucketAggs: [], @@ -178,7 +178,7 @@ describe('ElasticQueryBuilder', () => { }); it('with moving average', () => { - var query = builder.build({ + const query = builder.build({ metrics: [ { id: '3', @@ -195,7 +195,7 @@ describe('ElasticQueryBuilder', () => { bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '3' }], }); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.aggs['2']).not.toBe(undefined); expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined); @@ -203,7 +203,7 @@ describe('ElasticQueryBuilder', () => { }); it('with broken moving average', () => { - var query = builder.build({ + const query = builder.build({ metrics: [ { id: '3', @@ -224,7 +224,7 @@ describe('ElasticQueryBuilder', () => { bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '3' }], }); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.aggs['2']).not.toBe(undefined); expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined); @@ -233,7 +233,7 @@ describe('ElasticQueryBuilder', () => { }); it('with derivative', () => { - var query = builder.build({ + const query = builder.build({ metrics: [ { id: '3', @@ -249,7 +249,7 @@ describe('ElasticQueryBuilder', () => { bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '3' }], }); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.aggs['2']).not.toBe(undefined); expect(firstLevel.aggs['2'].derivative).not.toBe(undefined); @@ -257,7 +257,7 @@ describe('ElasticQueryBuilder', () => { }); it('with histogram', () => { - var query = builder.build({ + const query = builder.build({ metrics: [{ id: '1', type: 'count' }], bucketAggs: [ { @@ -269,7 +269,7 @@ describe('ElasticQueryBuilder', () => { ], }); - var firstLevel = query.aggs['3']; + const firstLevel = query.aggs['3']; expect(firstLevel.histogram.field).toBe('bytes'); expect(firstLevel.histogram.interval).toBe(10); expect(firstLevel.histogram.min_doc_count).toBe(2); @@ -277,7 +277,7 @@ describe('ElasticQueryBuilder', () => { }); it('with adhoc filters', () => { - var query = builder.build( + const query = builder.build( { metrics: [{ type: 'Count', id: '0' }], timeField: '@timestamp', diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts b/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts index 0102e5febfb..471d400037c 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts @@ -3,7 +3,7 @@ import * as queryDef from '../query_def'; describe('ElasticQueryDef', () => { describe('getPipelineAggOptions', () => { describe('with zero targets', () => { - var response = queryDef.getPipelineAggOptions([]); + const response = queryDef.getPipelineAggOptions([]); test('should return zero', () => { expect(response.length).toBe(0); @@ -11,11 +11,11 @@ describe('ElasticQueryDef', () => { }); describe('with count and sum targets', () => { - var targets = { + const targets = { metrics: [{ type: 'count', field: '@value' }, { type: 'sum', field: '@value' }], }; - var response = queryDef.getPipelineAggOptions(targets); + const response = queryDef.getPipelineAggOptions(targets); test('should return zero', () => { expect(response.length).toBe(2); @@ -23,11 +23,11 @@ describe('ElasticQueryDef', () => { }); describe('with count and moving average targets', () => { - var targets = { + const targets = { metrics: [{ type: 'count', field: '@value' }, { type: 'moving_avg', field: '@value' }], }; - var response = queryDef.getPipelineAggOptions(targets); + const response = queryDef.getPipelineAggOptions(targets); test('should return one', () => { expect(response.length).toBe(1); @@ -35,11 +35,11 @@ describe('ElasticQueryDef', () => { }); describe('with derivatives targets', () => { - var targets = { + const targets = { metrics: [{ type: 'derivative', field: '@value' }], }; - var response = queryDef.getPipelineAggOptions(targets); + const response = queryDef.getPipelineAggOptions(targets); test('should return zero', () => { expect(response.length).toBe(0); @@ -49,7 +49,7 @@ describe('ElasticQueryDef', () => { describe('isPipelineMetric', () => { describe('moving_avg', () => { - var result = queryDef.isPipelineAgg('moving_avg'); + const result = queryDef.isPipelineAgg('moving_avg'); test('is pipe line metric', () => { expect(result).toBe(true); @@ -57,7 +57,7 @@ describe('ElasticQueryDef', () => { }); describe('count', () => { - var result = queryDef.isPipelineAgg('count'); + const result = queryDef.isPipelineAgg('count'); test('is not pipe line metric', () => { expect(result).toBe(false); diff --git a/public/app/plugins/datasource/grafana-live/datasource.ts b/public/app/plugins/datasource/grafana-live/datasource.ts index 5cba43dd2f9..d861400b2c8 100644 --- a/public/app/plugins/datasource/grafana-live/datasource.ts +++ b/public/app/plugins/datasource/grafana-live/datasource.ts @@ -8,7 +8,7 @@ class DataObservable { } subscribe(options) { - var observable = liveSrv.subscribe(this.target.stream); + const observable = liveSrv.subscribe(this.target.stream); return observable.subscribe(data => { console.log('grafana stream ds data!', data); }); @@ -26,8 +26,8 @@ export class GrafanaStreamDS { return Promise.resolve({ data: [] }); } - var target = options.targets[0]; - var observable = new DataObservable(target); + const target = options.targets[0]; + const observable = new DataObservable(target); return Promise.resolve(observable); } diff --git a/public/app/plugins/datasource/graphite/specs/datasource.test.ts b/public/app/plugins/datasource/graphite/specs/datasource.test.ts index 826f2fed344..563f1047cdb 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource.test.ts @@ -324,7 +324,7 @@ function accessScenario(name, url, fn) { it('tracing headers should be added', () => { ctx.instanceSettings.url = url; - var ds = new GraphiteDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv); + const ds = new GraphiteDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv); ds.addTracingHeaders(httpOptions, options); fn(httpOptions); }); diff --git a/public/app/plugins/datasource/graphite/specs/gfunc.test.ts b/public/app/plugins/datasource/graphite/specs/gfunc.test.ts index 08373582e73..61a0e896b0f 100644 --- a/public/app/plugins/datasource/graphite/specs/gfunc.test.ts +++ b/public/app/plugins/datasource/graphite/specs/gfunc.test.ts @@ -2,7 +2,7 @@ import gfunc from '../gfunc'; describe('when creating func instance from func names', function() { it('should return func instance', function() { - var func = gfunc.createFuncInstance('sumSeries'); + const func = gfunc.createFuncInstance('sumSeries'); expect(func).toBeTruthy(); expect(func.def.name).toEqual('sumSeries'); expect(func.def.params.length).toEqual(1); @@ -11,18 +11,18 @@ describe('when creating func instance from func names', function() { }); it('should return func instance with shortName', function() { - var func = gfunc.createFuncInstance('sum'); + const func = gfunc.createFuncInstance('sum'); expect(func).toBeTruthy(); }); it('should return func instance from funcDef', function() { - var func = gfunc.createFuncInstance('sum'); - var func2 = gfunc.createFuncInstance(func.def); + const func = gfunc.createFuncInstance('sum'); + const func2 = gfunc.createFuncInstance(func.def); expect(func2).toBeTruthy(); }); it('func instance should have text representation', function() { - var func = gfunc.createFuncInstance('groupByNode'); + const func = gfunc.createFuncInstance('groupByNode'); func.params[0] = 5; func.params[1] = 'avg'; func.updateText(); @@ -32,62 +32,62 @@ describe('when creating func instance from func names', function() { describe('when rendering func instance', function() { it('should handle single metric param', function() { - var func = gfunc.createFuncInstance('sumSeries'); + const func = gfunc.createFuncInstance('sumSeries'); expect(func.render('hello.metric')).toEqual('sumSeries(hello.metric)'); }); it('should include default params if options enable it', function() { - var func = gfunc.createFuncInstance('scaleToSeconds', { + const func = gfunc.createFuncInstance('scaleToSeconds', { withDefaultParams: true, }); expect(func.render('hello')).toEqual('scaleToSeconds(hello, 1)'); }); it('should handle int or interval params with number', function() { - var func = gfunc.createFuncInstance('movingMedian'); + const func = gfunc.createFuncInstance('movingMedian'); func.params[0] = '5'; expect(func.render('hello')).toEqual('movingMedian(hello, 5)'); }); it('should handle int or interval params with interval string', function() { - var func = gfunc.createFuncInstance('movingMedian'); + const func = gfunc.createFuncInstance('movingMedian'); func.params[0] = '5min'; expect(func.render('hello')).toEqual("movingMedian(hello, '5min')"); }); it('should never quote boolean paramater', function() { - var func = gfunc.createFuncInstance('sortByName'); + const 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'); + const 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'); + const 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'); + const func = gfunc.createFuncInstance('groupByNode'); func.params[0] = 5; func.params[1] = 'avg'; expect(func.render('hello.metric')).toEqual("groupByNode(hello.metric, 5, 'avg')"); }); it('should handle function with no metric param', function() { - var func = gfunc.createFuncInstance('randomWalk'); + const func = gfunc.createFuncInstance('randomWalk'); func.params[0] = 'test'; expect(func.render(undefined)).toEqual("randomWalk('test')"); }); it('should handle function multiple series params', function() { - var func = gfunc.createFuncInstance('asPercent'); + const func = gfunc.createFuncInstance('asPercent'); func.params[0] = '#B'; expect(func.render('#A')).toEqual('asPercent(#A, #B)'); }); @@ -95,14 +95,14 @@ describe('when rendering func instance', function() { describe('when requesting function definitions', function() { it('should return function definitions', function() { - var funcIndex = gfunc.getFuncDefs('1.0'); + const funcIndex = gfunc.getFuncDefs('1.0'); expect(Object.keys(funcIndex).length).toBeGreaterThan(8); }); }); describe('when updating func param', function() { it('should update param value and update text representation', function() { - var func = gfunc.createFuncInstance('summarize', { + const func = gfunc.createFuncInstance('summarize', { withDefaultParams: true, }); func.updateParam('1h', 0); @@ -111,7 +111,7 @@ describe('when updating func param', function() { }); it('should parse numbers as float', function() { - var func = gfunc.createFuncInstance('scale'); + const func = gfunc.createFuncInstance('scale'); func.updateParam('0.001', 0); expect(func.params[0]).toBe('0.001'); }); @@ -119,13 +119,13 @@ describe('when updating func param', function() { describe('when updating func param with optional second parameter', function() { it('should update value and text', function() { - var func = gfunc.createFuncInstance('aliasByNode'); + const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('1', 0); expect(func.params[0]).toBe('1'); }); it('should slit text and put value in second param', function() { - var func = gfunc.createFuncInstance('aliasByNode'); + const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('4,-5', 0); expect(func.params[0]).toBe('4'); expect(func.params[1]).toBe('-5'); @@ -133,7 +133,7 @@ describe('when updating func param with optional second parameter', function() { }); it('should remove second param when empty string is set', function() { - var func = gfunc.createFuncInstance('aliasByNode'); + const func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('4,-5', 0); func.updateParam('', 1); expect(func.params[0]).toBe('4'); diff --git a/public/app/plugins/datasource/graphite/specs/lexer.test.ts b/public/app/plugins/datasource/graphite/specs/lexer.test.ts index c925e5cdaba..f00df17a725 100644 --- a/public/app/plugins/datasource/graphite/specs/lexer.test.ts +++ b/public/app/plugins/datasource/graphite/specs/lexer.test.ts @@ -2,8 +2,8 @@ import { Lexer } from '../lexer'; describe('when lexing graphite expression', function() { it('should tokenize metric expression', function() { - var lexer = new Lexer('metric.test.*.asd.count'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.test.*.asd.count'); + const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('metric'); expect(tokens[1].value).toBe('.'); expect(tokens[2].type).toBe('identifier'); @@ -12,36 +12,36 @@ describe('when lexing graphite expression', function() { }); it('should tokenize metric expression with dash', function() { - var lexer = new Lexer('metric.test.se1-server-*.asd.count'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.test.se1-server-*.asd.count'); + const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('identifier'); expect(tokens[4].value).toBe('se1-server-*'); }); it('should tokenize metric expression with dash2', function() { - var lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*'); + const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('net'); expect(tokens[2].value).toBe('192-168-1-1'); }); it('should tokenize metric expression with equal sign', function() { - var lexer = new Lexer('apps=test'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('apps=test'); + const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('apps=test'); }); it('simple function2', function() { - var lexer = new Lexer('offset(test.metric, -100)'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('offset(test.metric, -100)'); + const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); expect(tokens[4].type).toBe('identifier'); expect(tokens[6].type).toBe('number'); }); it('should tokenize metric expression with curly braces', function() { - var lexer = new Lexer('metric.se1-{first, second}.count'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.se1-{first, second}.count'); + const tokens = lexer.tokenize(); expect(tokens.length).toBe(10); expect(tokens[3].type).toBe('{'); expect(tokens[4].value).toBe('first'); @@ -50,8 +50,8 @@ describe('when lexing graphite expression', function() { }); it('should tokenize metric expression with number segments', function() { - var lexer = new Lexer('metric.10.12_10.test'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.10.12_10.test'); + const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); expect(tokens[2].type).toBe('identifier'); expect(tokens[2].value).toBe('10'); @@ -60,16 +60,16 @@ describe('when lexing graphite expression', function() { }); it('should tokenize metric expression with segment that start with number', function() { - var lexer = new Lexer('metric.001-server'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.001-server'); + const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); expect(tokens[2].type).toBe('identifier'); expect(tokens.length).toBe(3); }); it('should tokenize func call with numbered metric and number arg', function() { - var lexer = new Lexer('scale(metric.10, 15)'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('scale(metric.10, 15)'); + const tokens = lexer.tokenize(); expect(tokens[0].type).toBe('identifier'); expect(tokens[2].type).toBe('identifier'); expect(tokens[2].value).toBe('metric'); @@ -79,24 +79,24 @@ describe('when lexing graphite expression', function() { }); it('should tokenize metric with template parameter', function() { - var lexer = new Lexer('metric.[[server]].test'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.[[server]].test'); + const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); expect(tokens[2].value).toBe('[[server]]'); expect(tokens[4].type).toBe('identifier'); }); it('should tokenize metric with question mark', function() { - var lexer = new Lexer('metric.server_??.test'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('metric.server_??.test'); + const tokens = lexer.tokenize(); expect(tokens[2].type).toBe('identifier'); expect(tokens[2].value).toBe('server_??'); expect(tokens[4].type).toBe('identifier'); }); it('should handle error with unterminated string', function() { - var lexer = new Lexer("alias(metric, 'asd)"); - var tokens = lexer.tokenize(); + const lexer = new Lexer("alias(metric, 'asd)"); + const tokens = lexer.tokenize(); expect(tokens[0].value).toBe('alias'); expect(tokens[1].value).toBe('('); expect(tokens[2].value).toBe('metric'); @@ -107,15 +107,15 @@ describe('when lexing graphite expression', function() { }); it('should handle float parameters', function() { - var lexer = new Lexer('alias(metric, 0.002)'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('alias(metric, 0.002)'); + const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('number'); expect(tokens[4].value).toBe('0.002'); }); it('should handle bool parameters', function() { - var lexer = new Lexer('alias(metric, true, false)'); - var tokens = lexer.tokenize(); + const lexer = new Lexer('alias(metric, true, false)'); + const tokens = lexer.tokenize(); expect(tokens[4].type).toBe('bool'); expect(tokens[4].value).toBe('true'); expect(tokens[6].type).toBe('bool'); diff --git a/public/app/plugins/datasource/graphite/specs/parser.test.ts b/public/app/plugins/datasource/graphite/specs/parser.test.ts index 7964d9d257b..966eb213d64 100644 --- a/public/app/plugins/datasource/graphite/specs/parser.test.ts +++ b/public/app/plugins/datasource/graphite/specs/parser.test.ts @@ -2,8 +2,8 @@ import { Parser } from '../parser'; describe('when parsing', function() { it('simple metric expression', function() { - var parser = new Parser('metric.test.*.asd.count'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.test.*.asd.count'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments.length).toBe(5); @@ -11,8 +11,8 @@ describe('when parsing', function() { }); it('simple metric expression with numbers in segments', function() { - var parser = new Parser('metric.10.15_20.5'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.10.15_20.5'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments.length).toBe(4); @@ -22,8 +22,8 @@ describe('when parsing', function() { }); it('simple metric expression with curly braces', function() { - var parser = new Parser('metric.se1-{count, max}'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.se1-{count, max}'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments.length).toBe(2); @@ -31,8 +31,8 @@ describe('when parsing', function() { }); it('simple metric expression with curly braces at start of segment and with post chars', function() { - var parser = new Parser('metric.{count, max}-something.count'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.{count, max}-something.count'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments.length).toBe(3); @@ -40,31 +40,31 @@ describe('when parsing', function() { }); it('simple function', function() { - var parser = new Parser('sum(test)'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(test)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(1); }); it('simple function2', function() { - var parser = new Parser('offset(test.metric, -100)'); - var rootNode = parser.getAst(); + const parser = new Parser('offset(test.metric, -100)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params[0].type).toBe('metric'); expect(rootNode.params[1].type).toBe('number'); }); it('simple function with string arg', function() { - var parser = new Parser("randomWalk('test')"); - var rootNode = parser.getAst(); + const parser = new Parser("randomWalk('test')"); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(1); expect(rootNode.params[0].type).toBe('string'); }); it('function with multiple args', function() { - var parser = new Parser("sum(test, 1, 'test')"); - var rootNode = parser.getAst(); + const parser = new Parser("sum(test, 1, 'test')"); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(3); @@ -74,8 +74,8 @@ describe('when parsing', function() { }); it('function with nested function', function() { - var parser = new Parser('sum(scaleToSeconds(test, 1))'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(scaleToSeconds(test, 1))'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(1); @@ -87,8 +87,8 @@ describe('when parsing', function() { }); it('function with multiple series', function() { - var parser = new Parser('sum(test.test.*.count, test.timers.*.count)'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(test.test.*.count, test.timers.*.count)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params.length).toBe(2); @@ -97,8 +97,8 @@ describe('when parsing', function() { }); it('function with templated series', function() { - var parser = new Parser('sum(test.[[server]].count)'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(test.[[server]].count)'); + const rootNode = parser.getAst(); expect(rootNode.message).toBe(undefined); expect(rootNode.params[0].type).toBe('metric'); @@ -107,54 +107,54 @@ describe('when parsing', function() { }); it('invalid metric expression', function() { - var parser = new Parser('metric.test.*.asd.'); - var rootNode = parser.getAst(); + const parser = new Parser('metric.test.*.asd.'); + const rootNode = parser.getAst(); expect(rootNode.message).toBe('Expected metric identifier instead found end of string'); expect(rootNode.pos).toBe(19); }); it('invalid function expression missing closing parenthesis', function() { - var parser = new Parser('sum(test'); - var rootNode = parser.getAst(); + const parser = new Parser('sum(test'); + const rootNode = parser.getAst(); expect(rootNode.message).toBe('Expected closing parenthesis instead found end of string'); expect(rootNode.pos).toBe(9); }); it('unclosed string in function', function() { - var parser = new Parser("sum('test)"); - var rootNode = parser.getAst(); + const parser = new Parser("sum('test)"); + const rootNode = parser.getAst(); expect(rootNode.message).toBe('Unclosed string parameter'); expect(rootNode.pos).toBe(11); }); it('handle issue #69', function() { - var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))'); - var rootNode = parser.getAst(); + const parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); }); it('handle float function arguments', function() { - var parser = new Parser('scale(test, 0.002)'); - var rootNode = parser.getAst(); + const parser = new Parser('scale(test, 0.002)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params[1].type).toBe('number'); expect(rootNode.params[1].value).toBe(0.002); }); it('handle curly brace pattern at start', function() { - var parser = new Parser('{apps}.test'); - var rootNode = parser.getAst(); + const parser = new Parser('{apps}.test'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('metric'); expect(rootNode.segments[0].value).toBe('{apps}'); expect(rootNode.segments[1].value).toBe('test'); }); it('series parameters', function() { - var parser = new Parser('asPercent(#A, #B)'); - var rootNode = parser.getAst(); + const parser = new Parser('asPercent(#A, #B)'); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params[0].type).toBe('series-ref'); expect(rootNode.params[0].value).toBe('#A'); @@ -162,8 +162,8 @@ describe('when parsing', function() { }); it('series parameters, issue 2788', function() { - var parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)"); - var rootNode = parser.getAst(); + const parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)"); + const rootNode = parser.getAst(); expect(rootNode.type).toBe('function'); expect(rootNode.params[0].type).toBe('function'); expect(rootNode.params[1].value).toBe('10m'); @@ -171,8 +171,8 @@ describe('when parsing', function() { }); it('should parse metric expression with ip number segments', function() { - var parser = new Parser('5.10.123.5'); - var rootNode = parser.getAst(); + const parser = new Parser('5.10.123.5'); + const rootNode = parser.getAst(); expect(rootNode.segments[0].value).toBe('5'); expect(rootNode.segments[1].value).toBe('10'); expect(rootNode.segments[2].value).toBe('123'); diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts b/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts index 7c354e8aeeb..a62d5384ac6 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_query.test.ts @@ -1,11 +1,11 @@ import InfluxQuery from '../influx_query'; describe('InfluxQuery', function() { - var templateSrv = { replace: val => val }; + const templateSrv = { replace: val => val }; describe('render series with mesurement only', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', }, @@ -13,14 +13,14 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)'); }); }); describe('render series with policy only', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', policy: '5m_avg', @@ -29,7 +29,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "5m_avg"."cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)' ); @@ -38,7 +38,7 @@ describe('InfluxQuery', function() { describe('render series with math and alias', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [ @@ -54,7 +54,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(null)' ); @@ -63,7 +63,7 @@ describe('InfluxQuery', function() { describe('series with single tag only', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time', params: ['auto'] }], @@ -73,7 +73,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server\\\\1\') AND $timeFilter' + @@ -82,7 +82,7 @@ describe('InfluxQuery', function() { }); it('should switch regex operator with tag value is regex', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time', params: ['auto'] }], @@ -92,7 +92,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "cpu" WHERE ("app" =~ /e.*/) AND $timeFilter GROUP BY time($__interval)' ); @@ -101,7 +101,7 @@ describe('InfluxQuery', function() { describe('series with multiple tags only', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time', params: ['auto'] }], @@ -111,7 +111,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server1\' AND "app" = \'email\') AND ' + '$timeFilter GROUP BY time($__interval)' @@ -121,7 +121,7 @@ describe('InfluxQuery', function() { describe('series with tags OR condition', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time', params: ['auto'] }], @@ -131,7 +131,7 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe( 'SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server1\' OR "hostname" = \'server2\') AND ' + '$timeFilter GROUP BY time($__interval)' @@ -141,7 +141,7 @@ describe('InfluxQuery', function() { describe('query with value condition', function() { it('should not quote value', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [], @@ -151,14 +151,14 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT mean("value") FROM "cpu" WHERE ("value" > 5) AND $timeFilter'); }); }); describe('series with groupByTag', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', tags: [], @@ -168,14 +168,14 @@ describe('InfluxQuery', function() { {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($__interval), "host"'); }); }); describe('render series without group by', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }]], @@ -184,14 +184,14 @@ describe('InfluxQuery', function() { templateSrv, {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT "value" FROM "cpu" WHERE $timeFilter'); }); }); describe('render series without group by and fill', function() { it('should generate correct query', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }]], @@ -200,14 +200,14 @@ describe('InfluxQuery', function() { templateSrv, {} ); - var queryText = query.render(); + const queryText = query.render(); expect(queryText).toBe('SELECT "value" FROM "cpu" WHERE $timeFilter GROUP BY time($__interval) fill(0)'); }); }); describe('when adding group by part', function() { it('should add tag before fill', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [{ type: 'time' }, { type: 'fill' }], @@ -224,7 +224,7 @@ describe('InfluxQuery', function() { }); it('should add tag last if no fill', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', groupBy: [], @@ -241,7 +241,7 @@ describe('InfluxQuery', function() { describe('when adding select part', function() { it('should add mean after after field', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }]], @@ -256,7 +256,7 @@ describe('InfluxQuery', function() { }); it('should replace sum by mean', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]], @@ -271,7 +271,7 @@ describe('InfluxQuery', function() { }); it('should add math before alias', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'alias' }]], @@ -286,7 +286,7 @@ describe('InfluxQuery', function() { }); it('should add math last', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }]], @@ -301,7 +301,7 @@ describe('InfluxQuery', function() { }); it('should replace math', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }, { type: 'mean' }, { type: 'math' }]], @@ -316,7 +316,7 @@ describe('InfluxQuery', function() { }); it('should add math when one only query part', function() { - var query = new InfluxQuery( + const query = new InfluxQuery( { measurement: 'cpu', select: [[{ type: 'field', params: ['value'] }]], @@ -332,9 +332,9 @@ describe('InfluxQuery', function() { describe('when render adhoc filters', function() { it('should generate correct query segment', function() { - var query = new InfluxQuery({ measurement: 'cpu' }, templateSrv, {}); + const query = new InfluxQuery({ measurement: 'cpu' }, templateSrv, {}); - var queryText = query.renderAdhocFilters([ + const queryText = query.renderAdhocFilters([ { key: 'key1', operator: '=', value: 'value1' }, { key: 'key2', operator: '!=', value: 'value2' }, ]); diff --git a/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts b/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts index 8c8fee9ab9f..bb20db1ba76 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts @@ -2,7 +2,7 @@ import InfluxSeries from '../influx_series'; describe('when generating timeseries from influxdb response', function() { describe('given multiple fields for series', function() { - var options = { + const options = { alias: '', series: [ { @@ -15,8 +15,8 @@ describe('when generating timeseries from influxdb response', function() { }; describe('and no alias', function() { it('should generate multiple datapoints for each column', function() { - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result.length).toBe(3); expect(result[0].target).toBe('cpu.mean {app: test, server: server1}'); @@ -42,8 +42,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and simple alias', function() { it('should use alias', function() { options.alias = 'new series'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('new series'); expect(result[1].target).toBe('new series'); @@ -54,8 +54,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and alias patterns', function() { it('should replace patterns', function() { options.alias = 'alias: $m -> $tag_server ([[measurement]])'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('alias: cpu -> server1 (cpu)'); expect(result[1].target).toBe('alias: cpu -> server1 (cpu)'); @@ -65,7 +65,7 @@ describe('when generating timeseries from influxdb response', function() { }); describe('given measurement with default fieldname', function() { - var options = { + const options = { series: [ { name: 'cpu', @@ -84,8 +84,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and no alias', function() { it('should generate label with no field', function() { - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('cpu {app: test, server: server1}'); expect(result[1].target).toBe('cpu {app: test2, server: server2}'); @@ -94,7 +94,7 @@ describe('when generating timeseries from influxdb response', function() { }); describe('given two series', function() { - var options = { + const options = { alias: '', series: [ { @@ -114,8 +114,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and no alias', function() { it('should generate two time series', function() { - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result.length).toBe(2); expect(result[0].target).toBe('cpu.mean {app: test, server: server1}'); @@ -135,8 +135,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and simple alias', function() { it('should use alias', function() { options.alias = 'new series'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('new series'); }); @@ -145,8 +145,8 @@ describe('when generating timeseries from influxdb response', function() { describe('and alias patterns', function() { it('should replace patterns', function() { options.alias = 'alias: $m -> $tag_server ([[measurement]])'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('alias: cpu -> server1 (cpu)'); expect(result[1].target).toBe('alias: cpu -> server2 (cpu)'); @@ -155,7 +155,7 @@ describe('when generating timeseries from influxdb response', function() { }); describe('given measurement with dots', function() { - var options = { + const options = { alias: '', series: [ { @@ -169,15 +169,15 @@ describe('when generating timeseries from influxdb response', function() { it('should replace patterns', function() { options.alias = 'alias: $1 -> [[3]]'; - var series = new InfluxSeries(options); - var result = series.getTimeSeries(); + const series = new InfluxSeries(options); + const result = series.getTimeSeries(); expect(result[0].target).toBe('alias: prod -> count'); }); }); describe('given table response', function() { - var options = { + const options = { alias: '', series: [ { @@ -190,8 +190,8 @@ describe('when generating timeseries from influxdb response', function() { }; it('should return table', function() { - var series = new InfluxSeries(options); - var table = series.getTable(); + const series = new InfluxSeries(options); + const table = series.getTable(); expect(table.type).toBe('table'); expect(table.columns.length).toBe(5); @@ -201,7 +201,7 @@ describe('when generating timeseries from influxdb response', function() { }); describe('given table response from SHOW CARDINALITY', function() { - var options = { + const options = { alias: '', series: [ { @@ -213,8 +213,8 @@ describe('when generating timeseries from influxdb response', function() { }; it('should return table', function() { - var series = new InfluxSeries(options); - var table = series.getTable(); + const series = new InfluxSeries(options); + const table = series.getTable(); expect(table.type).toBe('table'); expect(table.columns.length).toBe(1); @@ -225,7 +225,7 @@ describe('when generating timeseries from influxdb response', function() { describe('given annotation response', function() { describe('with empty tagsColumn', function() { - var options = { + const options = { alias: '', annotation: {}, series: [ @@ -239,15 +239,15 @@ describe('when generating timeseries from influxdb response', function() { }; it('should multiple tags', function() { - var series = new InfluxSeries(options); - var annotations = series.getAnnotations(); + const series = new InfluxSeries(options); + const annotations = series.getAnnotations(); expect(annotations[0].tags.length).toBe(0); }); }); describe('given annotation response', function() { - var options = { + const options = { alias: '', annotation: { tagsColumn: 'datacenter, source', @@ -263,8 +263,8 @@ describe('when generating timeseries from influxdb response', function() { }; it('should multiple tags', function() { - var series = new InfluxSeries(options); - var annotations = series.getAnnotations(); + const series = new InfluxSeries(options); + const annotations = series.getAnnotations(); expect(annotations[0].tags.length).toBe(2); expect(annotations[0].tags[0]).toBe('America'); diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts index 30a9343f56e..d8b27f8b1bf 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_builder.test.ts @@ -3,139 +3,139 @@ import { InfluxQueryBuilder } from '../query_builder'; describe('InfluxQueryBuilder', function() { describe('when building explore queries', function() { it('should only have measurement condition in tag keys query given query with measurement', function() { - var builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }); - var query = builder.buildExploreQuery('TAG_KEYS'); + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }); + const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS FROM "cpu"'); }); it('should handle regex measurement in tag keys query', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '/.*/', tags: [], }); - var query = builder.buildExploreQuery('TAG_KEYS'); + const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS FROM /.*/'); }); it('should have no conditions in tags keys query given query with no measurement or tag', function() { - var builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - var query = builder.buildExploreQuery('TAG_KEYS'); + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS'); }); it('should have where condition in tag keys query with tags', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'host', value: 'se1' }], }); - var query = builder.buildExploreQuery('TAG_KEYS'); + const query = builder.buildExploreQuery('TAG_KEYS'); expect(query).toBe('SHOW TAG KEYS WHERE "host" = \'se1\''); }); it('should have no conditions in measurement query for query with no tags', function() { - var builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - var query = builder.buildExploreQuery('MEASUREMENTS'); + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('MEASUREMENTS'); expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); }); it('should have no conditions in measurement query for query with no tags and empty query', function() { - var builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - var query = builder.buildExploreQuery('MEASUREMENTS', undefined, ''); + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('MEASUREMENTS', undefined, ''); expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); }); it('should have WITH MEASUREMENT in measurement query for non-empty query with no tags', function() { - var builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); - var query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); + const builder = new InfluxQueryBuilder({ measurement: '', tags: [] }); + const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ LIMIT 100'); }); it('should have WITH MEASUREMENT WHERE in measurement query for non-empty query with tags', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'email' }], }); - var query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); + const query = builder.buildExploreQuery('MEASUREMENTS', undefined, 'something'); expect(query).toBe('SHOW MEASUREMENTS WITH MEASUREMENT =~ /something/ WHERE "app" = \'email\' LIMIT 100'); }); it('should have where condition in measurement query for query with tags', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'email' }], }); - var query = builder.buildExploreQuery('MEASUREMENTS'); + const query = builder.buildExploreQuery('MEASUREMENTS'); expect(query).toBe('SHOW MEASUREMENTS WHERE "app" = \'email\' LIMIT 100'); }); it('should have where tag name IN filter in tag values query for query with one tag', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '', tags: [{ key: 'app', value: 'asdsadsad' }], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES WITH KEY = "app"'); }); it('should have measurement tag condition and tag name IN filter in tag values query', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'app', value: 'email' }, { key: 'host', value: 'server1' }], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" = \'server1\''); }); it('should select from policy correctly if policy is specified', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', policy: 'one_week', tags: [{ key: 'app', value: 'email' }, { key: 'host', value: 'server1' }], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES FROM "one_week"."cpu" WITH KEY = "app" WHERE "host" = \'server1\''); }); it('should not include policy when policy is default', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', policy: 'default', tags: [], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app"'); }); it('should switch to regex operator in tag condition', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'host', value: '/server.*/' }], }); - var query = builder.buildExploreQuery('TAG_VALUES', 'app'); + const query = builder.buildExploreQuery('TAG_VALUES', 'app'); expect(query).toBe('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/'); }); it('should build show field query', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [{ key: 'app', value: 'email' }], }); - var query = builder.buildExploreQuery('FIELDS'); + const query = builder.buildExploreQuery('FIELDS'); expect(query).toBe('SHOW FIELD KEYS FROM "cpu"'); }); it('should build show field query with regexp', function() { - var builder = new InfluxQueryBuilder({ + const builder = new InfluxQueryBuilder({ measurement: '/$var/', tags: [{ key: 'app', value: 'email' }], }); - var query = builder.buildExploreQuery('FIELDS'); + const query = builder.buildExploreQuery('FIELDS'); expect(query).toBe('SHOW FIELD KEYS FROM /$var/'); }); it('should build show retention policies query', function() { - var builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }, 'site'); - var query = builder.buildExploreQuery('RETENTION POLICIES'); + const builder = new InfluxQueryBuilder({ measurement: 'cpu', tags: [] }, 'site'); + const query = builder.buildExploreQuery('RETENTION POLICIES'); expect(query).toBe('SHOW RETENTION POLICIES on "site"'); }); }); diff --git a/public/app/plugins/datasource/influxdb/specs/query_part.test.ts b/public/app/plugins/datasource/influxdb/specs/query_part.test.ts index e9e6d216c1e..264c695c8a7 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_part.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_part.test.ts @@ -3,7 +3,7 @@ import queryPart from '../query_part'; describe('InfluxQueryPart', () => { describe('series with measurement only', () => { it('should handle nested function parts', () => { - var part = queryPart.create({ + const part = queryPart.create({ type: 'derivative', params: ['10s'], }); @@ -13,7 +13,7 @@ describe('InfluxQueryPart', () => { }); it('should nest spread function', () => { - var part = queryPart.create({ + const part = queryPart.create({ type: 'spread', }); @@ -22,7 +22,7 @@ describe('InfluxQueryPart', () => { }); it('should handle suffix parts', () => { - var part = queryPart.create({ + const part = queryPart.create({ type: 'math', params: ['/ 100'], }); @@ -32,7 +32,7 @@ describe('InfluxQueryPart', () => { }); it('should handle alias parts', () => { - var part = queryPart.create({ + const part = queryPart.create({ type: 'alias', params: ['test'], }); @@ -42,7 +42,7 @@ describe('InfluxQueryPart', () => { }); it('should nest distinct when count is selected', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -52,7 +52,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'distinct', category: queryPart.getCategories().Aggregations, }); @@ -64,7 +64,7 @@ describe('InfluxQueryPart', () => { }); it('should convert to count distinct when distinct is selected and count added', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -74,7 +74,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'count', category: queryPart.getCategories().Aggregations, }); @@ -86,7 +86,7 @@ describe('InfluxQueryPart', () => { }); it('should replace count distinct if an aggregation is selected', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -100,7 +100,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'mean', category: queryPart.getCategories().Selectors, }); @@ -112,7 +112,7 @@ describe('InfluxQueryPart', () => { }); it('should not allowed nested counts when count distinct is selected', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -126,7 +126,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'count', category: queryPart.getCategories().Aggregations, }); @@ -139,7 +139,7 @@ describe('InfluxQueryPart', () => { }); it('should not remove count distinct when distinct is added', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -153,7 +153,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'distinct', category: queryPart.getCategories().Aggregations, }); @@ -166,7 +166,7 @@ describe('InfluxQueryPart', () => { }); it('should remove distinct when sum aggregation is selected', () => { - var selectParts = [ + const selectParts = [ queryPart.create({ type: 'field', category: queryPart.getCategories().Fields, @@ -176,7 +176,7 @@ describe('InfluxQueryPart', () => { category: queryPart.getCategories().Aggregations, }), ]; - var partModel = queryPart.create({ + const partModel = queryPart.create({ type: 'sum', category: queryPart.getCategories().Aggregations, }); diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts b/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts index 525508b2c1d..cca78974fe3 100644 --- a/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts +++ b/public/app/plugins/datasource/influxdb/specs/response_parser.test.ts @@ -5,8 +5,8 @@ describe('influxdb response parser', () => { const parser = new ResponseParser(); describe('SHOW TAG response', () => { - var query = 'SHOW TAG KEYS FROM "cpu"'; - var response = { + const query = 'SHOW TAG KEYS FROM "cpu"'; + const response = { results: [ { series: [ @@ -20,7 +20,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('expects three results', () => { expect(_.size(result)).toBe(3); @@ -28,10 +28,10 @@ describe('influxdb response parser', () => { }); describe('SHOW TAG VALUES response', () => { - var query = 'SHOW TAG VALUES FROM "cpu" WITH KEY = "hostname"'; + const query = 'SHOW TAG VALUES FROM "cpu" WITH KEY = "hostname"'; describe('response from 0.10.0', () => { - var response = { + const response = { results: [ { series: [ @@ -45,7 +45,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should get two responses', () => { expect(_.size(result)).toBe(2); @@ -55,7 +55,7 @@ describe('influxdb response parser', () => { }); describe('response from 0.12.0', () => { - var response = { + const response = { results: [ { series: [ @@ -74,7 +74,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should get two responses', () => { expect(_.size(result)).toBe(3); @@ -86,8 +86,8 @@ describe('influxdb response parser', () => { }); describe('SELECT response', () => { - var query = 'SELECT "usage_iowait" FROM "cpu" LIMIT 10'; - var response = { + const query = 'SELECT "usage_iowait" FROM "cpu" LIMIT 10'; + const response = { results: [ { series: [ @@ -101,7 +101,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should return second column', () => { expect(_.size(result)).toBe(3); @@ -112,10 +112,10 @@ describe('influxdb response parser', () => { }); describe('SHOW FIELD response', () => { - var query = 'SHOW FIELD KEYS FROM "cpu"'; + const query = 'SHOW FIELD KEYS FROM "cpu"'; describe('response from pre-1.0', () => { - var response = { + const response = { results: [ { series: [ @@ -129,7 +129,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should get two responses', () => { expect(_.size(result)).toBe(1); @@ -137,7 +137,7 @@ describe('influxdb response parser', () => { }); describe('response from 1.0', () => { - var response = { + const response = { results: [ { series: [ @@ -151,7 +151,7 @@ describe('influxdb response parser', () => { ], }; - var result = parser.parse(query, response); + const result = parser.parse(query, response); it('should return first column', () => { expect(_.size(result)).toBe(1); diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts index befa39fc80e..e7e53c0dd5b 100644 --- a/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts +++ b/public/app/plugins/datasource/opentsdb/specs/datasource.test.ts @@ -16,8 +16,8 @@ describe('opentsdb', () => { }); describe('When performing metricFindQuery', () => { - var results; - var requestOptions; + let results; + let requestOptions; beforeEach(async () => { ctx.backendSrv.datasourceRequest = await function(options) { diff --git a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts index 58a10b21207..6fdcd29aecd 100644 --- a/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/opentsdb/specs/query_ctrl.test.ts @@ -1,7 +1,7 @@ import { OpenTsQueryCtrl } from '../query_ctrl'; describe('OpenTsQueryCtrl', () => { - var ctx = { + const ctx = { target: { target: '' }, datasource: { tsdbVersion: '', diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index fd963f7986e..846a00212d0 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -409,14 +409,14 @@ const timeSrv = { describe('PrometheusDatasource', () => { describe('When querying prometheus with one target using query editor target spec', async () => { - var results; - var query = { + let results; + const 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 = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60'; beforeEach(async () => { @@ -453,12 +453,12 @@ describe('PrometheusDatasource', () => { }); }); describe('When querying prometheus with one target which return multiple series', () => { - var results; - var start = 60; - var end = 360; - var step = 60; + let results; + const start = 60; + const end = 360; + const step = 60; - var query = { + const query = { range: { from: time({ seconds: start }), to: time({ seconds: end }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], interval: '60s', @@ -505,7 +505,7 @@ describe('PrometheusDatasource', () => { expect(results.data[0].datapoints[1][0]).toBe(3846); }); it('should fill null after last datapoint in response', () => { - var length = (end - start) / step + 1; + const 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); @@ -521,9 +521,9 @@ describe('PrometheusDatasource', () => { }); }); 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 = { + let results; + const urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; + const query = { range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], interval: '60s', @@ -563,9 +563,9 @@ describe('PrometheusDatasource', () => { }); }); describe('When performing annotationQuery', () => { - var results; + let results; - var options = { + const options = { annotation: { expr: 'ALERTS{alertstate="firing"}', tagKeys: 'job', @@ -617,8 +617,8 @@ describe('PrometheusDatasource', () => { }); describe('When resultFormat is table and instant = true', () => { - var results; - var query = { + let results; + const query = { range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], interval: '60s', @@ -653,7 +653,7 @@ describe('PrometheusDatasource', () => { }); describe('The "step" query parameter', () => { - var response = { + const response = { status: 'success', data: { data: { @@ -686,13 +686,13 @@ describe('PrometheusDatasource', () => { }); it('step should never go below 1', async () => { - var query = { + const 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'; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -702,7 +702,7 @@ describe('PrometheusDatasource', () => { }); it('should be auto interval when greater than min interval', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -713,7 +713,7 @@ describe('PrometheusDatasource', () => { ], interval: '10s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -722,15 +722,15 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should result in querying fewer than 11000 data points', async () => { - var query = { + const 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'; + const end = 7 * 60 * 60; + const start = 60 * 60; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -739,7 +739,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should not apply min interval when interval * intervalFactor greater', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -752,7 +752,7 @@ describe('PrometheusDatasource', () => { interval: '5s', }; // times get rounded up to interval - var urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50'; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -761,7 +761,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should apply min interval when interval * intervalFactor smaller', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -773,7 +773,7 @@ describe('PrometheusDatasource', () => { ], interval: '5s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -782,7 +782,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should apply intervalFactor to auto interval when greater', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -795,7 +795,7 @@ describe('PrometheusDatasource', () => { interval: '10s', }; // times get aligned to interval - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100'; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -804,7 +804,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should not not be affected by the 11000 data points limit when large enough', async () => { - var query = { + const query = { // 1 week range range: { from: time({}), to: time({ hours: 7 * 24 }) }, targets: [ @@ -815,9 +815,9 @@ describe('PrometheusDatasource', () => { ], 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'; + const end = 7 * 24 * 60 * 60; + const start = 0; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -826,7 +826,7 @@ describe('PrometheusDatasource', () => { expect(res.url).toBe(urlExpected); }); it('should be determined by the 11000 data points limit when too small', async () => { - var query = { + const query = { // 1 week range range: { from: time({}), to: time({ hours: 7 * 24 }) }, targets: [ @@ -837,9 +837,9 @@ describe('PrometheusDatasource', () => { ], 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'; + const end = 7 * 24 * 60 * 60; + const start = 0; + const 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, backendSrv, templateSrv, timeSrv); await ctx.ds.query(query); @@ -850,7 +850,7 @@ describe('PrometheusDatasource', () => { }); describe('The __interval and __interval_ms template variables', () => { - var response = { + const response = { status: 'success', data: { data: { @@ -861,7 +861,7 @@ describe('PrometheusDatasource', () => { }; it('should be unchanged when auto interval is greater than min interval', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -877,7 +877,7 @@ describe('PrometheusDatasource', () => { }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=60&end=420&step=10'; @@ -902,7 +902,7 @@ describe('PrometheusDatasource', () => { }); }); it('should be min interval when it is greater than auto interval', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -917,7 +917,7 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=60&end=420&step=10'; @@ -941,7 +941,7 @@ describe('PrometheusDatasource', () => { }); }); it('should account for intervalFactor', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -957,7 +957,7 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 10 * 1000, value: 10 * 1000 }, }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=0&end=500&step=100'; @@ -986,7 +986,7 @@ describe('PrometheusDatasource', () => { expect(query.scopedVars.__interval_ms.value).toBe(10 * 1000); }); it('should be interval * intervalFactor when greater than min interval', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -1002,7 +1002,7 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=50&end=450&step=50'; @@ -1027,7 +1027,7 @@ describe('PrometheusDatasource', () => { }); }); it('should be min interval when greater than interval * intervalFactor', async () => { - var query = { + const query = { // 6 minute range range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) }, targets: [ @@ -1043,7 +1043,7 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, }, }; - var urlExpected = + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=60&end=420&step=15'; @@ -1067,7 +1067,7 @@ describe('PrometheusDatasource', () => { }); }); it('should be determined by the 11000 data points limit, accounting for intervalFactor', async () => { - var query = { + const query = { // 1 week range range: { from: time({}), to: time({ hours: 7 * 24 }) }, targets: [ @@ -1082,9 +1082,9 @@ describe('PrometheusDatasource', () => { __interval_ms: { text: 5 * 1000, value: 5 * 1000 }, }, }; - var end = 7 * 24 * 60 * 60; - var start = 0; - var urlExpected = + const end = 7 * 24 * 60 * 60; + const start = 0; + const urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[$__interval])') + '&start=' + @@ -1115,7 +1115,7 @@ describe('PrometheusDatasource', () => { }); describe('PrometheusDatasource for POST', () => { - // var ctx = new helpers.ServiceTestContext(); + // const ctx = new helpers.ServiceTestContext(); const instanceSettings = { url: 'proxied', directUrl: 'direct', @@ -1125,15 +1125,15 @@ describe('PrometheusDatasource for POST', () => { }; describe('When querying prometheus with one target using query editor target spec', () => { - var results; - var urlExpected = 'proxied/api/v1/query_range'; - var dataExpected = { + let results; + const urlExpected = 'proxied/api/v1/query_range'; + const dataExpected = { query: 'test{job="testjob"}', start: 1 * 60, end: 3 * 60, step: 60, }; - var query = { + const query = { range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], interval: '60s', diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts index ac85e1374bb..0ccb79a5d1f 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.test.ts @@ -11,7 +11,7 @@ describe('Prometheus Result Transformer', () => { }); describe('When resultFormat is table', () => { - var response = { + const response = { status: 'success', data: { resultType: 'matrix', @@ -33,7 +33,7 @@ describe('Prometheus Result Transformer', () => { }; it('should return table model', () => { - var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); + const table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); expect(table.type).toBe('table'); expect(table.rows).toEqual([ [1443454528000, 'test', '', 'testjob', 3846], @@ -49,7 +49,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'); + const table = ctx.resultTransformer.transformMetricDataToTable(response.data.result, 2, 'B'); expect(table.type).toBe('table'); expect(table.columns).toMatchObject([ { text: 'Time', type: 'time' }, @@ -62,7 +62,7 @@ describe('Prometheus Result Transformer', () => { }); describe('When resultFormat is table and instant = true', () => { - var response = { + const response = { status: 'success', data: { resultType: 'vector', @@ -76,7 +76,7 @@ describe('Prometheus Result Transformer', () => { }; it('should return table model', () => { - var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); + const table = ctx.resultTransformer.transformMetricDataToTable(response.data.result); expect(table.type).toBe('table'); expect(table.rows).toEqual([[1443454528000, 'test', 'testjob', 3846]]); expect(table.columns).toMatchObject([ @@ -89,7 +89,7 @@ describe('Prometheus Result Transformer', () => { }); describe('When resultFormat is heatmap', () => { - var response = { + const response = { status: 'success', data: { resultType: 'matrix', diff --git a/public/app/plugins/panel/graph/specs/data_processor.test.ts b/public/app/plugins/panel/graph/specs/data_processor.test.ts index 3ae34e277c8..2e8d4eb6eab 100644 --- a/public/app/plugins/panel/graph/specs/data_processor.test.ts +++ b/public/app/plugins/panel/graph/specs/data_processor.test.ts @@ -1,11 +1,11 @@ import { DataProcessor } from '../data_processor'; describe('Graph DataProcessor', function() { - var panel: any = { + const panel: any = { xaxis: {}, }; - var processor = new DataProcessor(panel); + const processor = new DataProcessor(panel); describe('Given default xaxis options and query that returns docs', () => { beforeEach(() => { @@ -29,7 +29,7 @@ describe('Graph DataProcessor', function() { }); describe('getDataFieldNames(', () => { - var dataList = [ + const dataList = [ { type: 'docs', datapoints: [ @@ -46,7 +46,7 @@ describe('Graph DataProcessor', function() { ]; it('Should return all field names', () => { - var fields = processor.getDataFieldNames(dataList, false); + const fields = processor.getDataFieldNames(dataList, false); expect(fields).toContain('hostname'); expect(fields).toContain('valueField'); expect(fields).toContain('nested.prop1'); @@ -54,7 +54,7 @@ describe('Graph DataProcessor', function() { }); it('Should return all number fields', () => { - var fields = processor.getDataFieldNames(dataList, true); + const fields = processor.getDataFieldNames(dataList, true); expect(fields).toContain('valueField'); expect(fields).toContain('nested.value2'); }); diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index 2ae76bb9c9c..64dd1de01ed 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -243,7 +243,7 @@ describe('grafanaGraph', function() { }); it('should apply axis transform, autoscaling (if necessary) and ticks', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; + const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); expect(axisAutoscale.min).toBeCloseTo(0.001); @@ -256,7 +256,7 @@ describe('grafanaGraph', function() { expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBe(10000); } - var axisFixedscale = ctx.plotOptions.yaxes[1]; + const axisFixedscale = ctx.plotOptions.yaxes[1]; expect(axisFixedscale.min).toBe(0.05); expect(axisFixedscale.max).toBe(1500); expect(axisFixedscale.ticks.length).toBe(5); @@ -278,7 +278,7 @@ describe('grafanaGraph', function() { }); it('should not set min and max and should create some fake ticks', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; + const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); expect(axisAutoscale.min).toBe(undefined); @@ -304,7 +304,7 @@ describe('grafanaGraph', function() { }); }); it('should set min to 0.1 and add a tick for 0.1', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; + const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.transform(100)).toBe(2); expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001); expect(axisAutoscale.min).toBe(0.1); @@ -331,7 +331,7 @@ describe('grafanaGraph', function() { }); it('should regenerate ticks so that if fits on the y-axis', function() { - var axisAutoscale = ctx.plotOptions.yaxes[0]; + const axisAutoscale = ctx.plotOptions.yaxes[0]; expect(axisAutoscale.min).toBe(0.1); expect(axisAutoscale.ticks.length).toBe(8); expect(axisAutoscale.ticks[0]).toBe(0.1); @@ -432,7 +432,7 @@ describe('grafanaGraph', function() { }); it('should show percentage', function() { - var axis = ctx.plotOptions.yaxes[0]; + const axis = ctx.plotOptions.yaxes[0]; expect(axis.tickFormatter(100, axis)).toBe('100%'); }); }); @@ -448,7 +448,7 @@ describe('grafanaGraph', function() { }); it('should format dates as hours minutes', function() { - var axis = ctx.plotOptions.xaxis; + const axis = ctx.plotOptions.xaxis; expect(axis.timeformat).toBe('%H:%M'); }); }); @@ -462,7 +462,7 @@ describe('grafanaGraph', function() { }); it('should format dates as month days', function() { - var axis = ctx.plotOptions.xaxis; + const axis = ctx.plotOptions.xaxis; expect(axis.timeformat).toBe('%m/%d'); }); }); diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts index 49efa8d4120..2feb94a5626 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl.test.ts @@ -43,7 +43,7 @@ describe('GraphCtrl', () => { describe('when time series are outside range', () => { beforeEach(() => { - var data = [ + const data = [ { target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]], @@ -61,14 +61,14 @@ describe('GraphCtrl', () => { describe('when time series are inside range', () => { beforeEach(() => { - var range = { + const range = { from: moment() .subtract(1, 'days') .valueOf(), to: moment().valueOf(), }; - var data = [ + const data = [ { target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]], @@ -86,7 +86,7 @@ describe('GraphCtrl', () => { describe('datapointsCount given 2 series', () => { beforeEach(() => { - var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; + const data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; ctx.ctrl.onDataReceived(data); }); diff --git a/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts b/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts index baebf2c5930..ecc6ce0fb21 100644 --- a/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts +++ b/public/app/plugins/panel/graph/specs/graph_tooltip.test.ts @@ -3,18 +3,18 @@ jest.mock('app/core/core', () => ({})); import $ from 'jquery'; import GraphTooltip from '../graph_tooltip'; -var scope = { +const scope = { appEvent: jest.fn(), onAppEvent: jest.fn(), ctrl: {}, }; -var elem = $('
    '); -var dashboard = {}; -var getSeriesFn; +const elem = $('
    '); +const dashboard = {}; +const getSeriesFn = () => {}; function describeSharedTooltip(desc, fn) { - var ctx: any = {}; + const ctx: any = {}; ctx.ctrl = scope.ctrl; ctx.ctrl.panel = { tooltip: { @@ -31,7 +31,7 @@ function describeSharedTooltip(desc, fn) { describe(desc, function() { beforeEach(function() { ctx.setupFn(); - var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); + const tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos); }); @@ -40,28 +40,28 @@ function describeSharedTooltip(desc, fn) { } describe('findHoverIndexFromData', function() { - var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); - var series = { + const tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn); + const series = { data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]], }; it('should return 0 if posX out of lower bounds', function() { - var posX = 99; + const posX = 99; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(0); }); it('should return n - 1 if posX out of upper bounds', function() { - var posX = 108; + const posX = 108; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(series.data.length - 1); }); it('should return i if posX in series', function() { - var posX = 104; + const posX = 104; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4); }); it('should return i if posX not in series and i + 1 > posX', function() { - var posX = 104.9; + const posX = 104.9; expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4); }); }); diff --git a/public/app/plugins/panel/graph/specs/threshold_manager.test.ts b/public/app/plugins/panel/graph/specs/threshold_manager.test.ts index 4a7a46fc6b0..ecbc382923e 100644 --- a/public/app/plugins/panel/graph/specs/threshold_manager.test.ts +++ b/public/app/plugins/panel/graph/specs/threshold_manager.test.ts @@ -5,7 +5,7 @@ import { ThresholdManager } from '../threshold_manager'; describe('ThresholdManager', function() { function plotOptionsScenario(desc, func) { describe(desc, function() { - var ctx: any = { + const ctx: any = { panel: { thresholds: [], }, @@ -17,9 +17,9 @@ describe('ThresholdManager', function() { ctx.setup = function(thresholds, data) { ctx.panel.thresholds = thresholds; - var manager = new ThresholdManager(ctx.panelCtrl); + const manager = new ThresholdManager(ctx.panelCtrl); if (data !== undefined) { - var element = angular.element('
    '); + const element = angular.element('
    '); manager.prepare(element, data); } manager.addFlotOptions(ctx.options, ctx.panel); @@ -34,7 +34,7 @@ describe('ThresholdManager', function() { ctx.setup([{ op: 'gt', value: 300, fill: true, line: true, colorMode: 'critical' }]); it('should add fill for threshold with fill: true', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); expect(markings[0].yaxis.to).toBe(Infinity); @@ -42,7 +42,7 @@ describe('ThresholdManager', function() { }); it('should add line', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(300); expect(markings[1].yaxis.to).toBe(300); expect(markings[1].color).toBe('rgba(237, 46, 24, 0.60)'); @@ -56,13 +56,13 @@ describe('ThresholdManager', function() { ]); it('should add fill for first thresholds to next threshold', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(200); expect(markings[0].yaxis.to).toBe(300); }); it('should add fill for last thresholds to infinity', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(300); expect(markings[1].yaxis.to).toBe(Infinity); }); @@ -75,13 +75,13 @@ describe('ThresholdManager', function() { ]); it('should add fill for first thresholds to next threshold', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); expect(markings[0].yaxis.to).toBe(200); }); it('should add fill for last thresholds to itself', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(200); expect(markings[1].yaxis.to).toBe(200); }); @@ -94,20 +94,20 @@ describe('ThresholdManager', function() { ]); it('should add fill for first thresholds to next threshold', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(300); expect(markings[0].yaxis.to).toBe(Infinity); }); it('should add fill for last thresholds to itself', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].yaxis.from).toBe(200); expect(markings[1].yaxis.to).toBe(-Infinity); }); }); plotOptionsScenario('for threshold on two Y axes', ctx => { - var data = new Array(2); + const data = new Array(2); data[0] = new TimeSeries({ datapoints: [[0, 1], [300, 2]], alias: 'left', @@ -127,12 +127,12 @@ describe('ThresholdManager', function() { ); it('should add first threshold for left axis', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[0].yaxis.from).toBe(100); }); it('should add second threshold for right axis', function() { - var markings = ctx.options.grid.markings; + const markings = ctx.options.grid.markings; expect(markings[1].y2axis.from).toBe(200); }); }); diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts index d9d929a2697..8e1623c7d6f 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts @@ -25,7 +25,7 @@ describe('HeatmapCtrl', function() { describe('when time series are outside range', function() { beforeEach(function() { - var data = [ + const data = [ { target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]], @@ -43,14 +43,14 @@ describe('HeatmapCtrl', function() { describe('when time series are inside range', function() { beforeEach(function() { - var range = { + const range = { from: moment() .subtract(1, 'days') .valueOf(), to: moment().valueOf(), }; - var data = [ + const data = [ { target: 'test.cpu1', datapoints: [[45, range.from + 1000], [60, range.from + 10000]], @@ -68,7 +68,7 @@ describe('HeatmapCtrl', function() { describe('datapointsCount given 2 series', function() { beforeEach(function() { - var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; + const data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }]; ctx.ctrl.onDataReceived(data); }); diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts index 028200147f7..114cdf132e1 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat_panel.test.ts @@ -3,7 +3,7 @@ import { getColorForValue } from '../module'; describe('grafanaSingleStat', function() { describe('legacy thresholds', () => { describe('positive thresholds', () => { - var data: any = { + const data: any = { colorMap: ['green', 'yellow', 'red'], thresholds: [20, 50], }; @@ -39,7 +39,7 @@ describe('grafanaSingleStat', function() { }); describe('negative thresholds', () => { - var data: any = { + const data: any = { colorMap: ['green', 'yellow', 'red'], thresholds: [0, 20], }; @@ -58,7 +58,7 @@ describe('grafanaSingleStat', function() { }); describe('negative thresholds', () => { - var data: any = { + const data: any = { colorMap: ['green', 'yellow', 'red'], thresholds: [-27, 20], }; diff --git a/public/app/plugins/panel/table/specs/renderer.test.ts b/public/app/plugins/panel/table/specs/renderer.test.ts index 22957d1aa66..b66984ba223 100644 --- a/public/app/plugins/panel/table/specs/renderer.test.ts +++ b/public/app/plugins/panel/table/specs/renderer.test.ts @@ -4,7 +4,7 @@ import { TableRenderer } from '../renderer'; describe('when rendering table', () => { describe('given 13 columns', () => { - var table = new TableModel(); + const table = new TableModel(); table.columns = [ { text: 'Time' }, { text: 'Value' }, @@ -24,7 +24,7 @@ describe('when rendering table', () => { [1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2], ]; - var panel = { + const panel = { pageSize: 10, styles: [ { @@ -163,11 +163,11 @@ describe('when rendering table', () => { ], }; - var sanitize = function(value) { + const sanitize = function(value) { return 'sanitized'; }; - var templateSrv = { + const templateSrv = { replace: function(value, scopedVars) { if (scopedVars) { // For testing variables replacement in link @@ -179,75 +179,75 @@ describe('when rendering table', () => { }, }; - var renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); + const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); it('time column should be formated', () => { - var html = renderer.renderCell(0, 0, 1388556366666); + const html = renderer.renderCell(0, 0, 1388556366666); expect(html).toBe('
    '); }); it('undefined time column should be rendered as -', () => { - var html = renderer.renderCell(0, 0, undefined); + const html = renderer.renderCell(0, 0, undefined); expect(html).toBe(''); }); it('null time column should be rendered as -', () => { - var html = renderer.renderCell(0, 0, null); + const html = renderer.renderCell(0, 0, null); expect(html).toBe(''); }); it('number column with unit specified should ignore style unit', () => { - var html = renderer.renderCell(5, 0, 1230); + const html = renderer.renderCell(5, 0, 1230); expect(html).toBe(''); }); it('number column should be formated', () => { - var html = renderer.renderCell(1, 0, 1230); + const html = renderer.renderCell(1, 0, 1230); expect(html).toBe(''); }); it('number style should ignore string values', () => { - var html = renderer.renderCell(1, 0, 'asd'); + const html = renderer.renderCell(1, 0, 'asd'); expect(html).toBe(''); }); it('colored cell should have style', () => { - var html = renderer.renderCell(2, 0, 40); + const html = renderer.renderCell(2, 0, 40); expect(html).toBe(''); }); it('colored cell should have style', () => { - var html = renderer.renderCell(2, 0, 55); + const html = renderer.renderCell(2, 0, 55); expect(html).toBe(''); }); it('colored cell should have style', () => { - var html = renderer.renderCell(2, 0, 85); + const html = renderer.renderCell(2, 0, 85); expect(html).toBe(''); }); it('unformated undefined should be rendered as string', () => { - var html = renderer.renderCell(3, 0, 'value'); + const html = renderer.renderCell(3, 0, 'value'); expect(html).toBe(''); }); it('string style with escape html should return escaped html', () => { - var html = renderer.renderCell(4, 0, '&breaking
    the
    row'); + const html = renderer.renderCell(4, 0, '&breaking
    the
    row'); expect(html).toBe('
    '); }); it('undefined formater should return escaped html', () => { - var html = renderer.renderCell(3, 0, '&breaking
    the
    row'); + const html = renderer.renderCell(3, 0, '&breaking
    the
    row'); expect(html).toBe('
    '); }); it('undefined value should render as -', () => { - var html = renderer.renderCell(3, 0, undefined); + const html = renderer.renderCell(3, 0, undefined); expect(html).toBe(''); }); it('sanitized value should render as', () => { - var html = renderer.renderCell(6, 0, 'text link'); + const html = renderer.renderCell(6, 0, 'text link'); expect(html).toBe(''); }); @@ -264,8 +264,8 @@ describe('when rendering table', () => { }); it('link should render as', () => { - var html = renderer.renderCell(7, 0, 'host1'); - var expectedHtml = ` + const html = renderer.renderCell(7, 0, 'host1'); + const expectedHtml = ` '); }); it('numeric value should be mapped to text', () => { - var html = renderer.renderCell(9, 0, 1); + const html = renderer.renderCell(9, 0, 1); expect(html).toBe(''); }); it('string numeric value should be mapped to text', () => { - var html = renderer.renderCell(9, 0, '0'); + const html = renderer.renderCell(9, 0, '0'); expect(html).toBe(''); }); it('string value should be mapped to text', () => { - var html = renderer.renderCell(9, 0, 'HELLO WORLD'); + const html = renderer.renderCell(9, 0, 'HELLO WORLD'); expect(html).toBe(''); }); it('array column value should be mapped to text', () => { - var html = renderer.renderCell(9, 0, ['value1', 'value2']); + const html = renderer.renderCell(9, 0, ['value1', 'value2']); expect(html).toBe(''); }); it('value should be mapped to text (range)', () => { - var html = renderer.renderCell(10, 0, 2); + const html = renderer.renderCell(10, 0, 2); expect(html).toBe(''); }); it('value should be mapped to text (range)', () => { - var html = renderer.renderCell(10, 0, 5); + const html = renderer.renderCell(10, 0, 5); expect(html).toBe(''); }); it('array column value should not be mapped to text', () => { - var html = renderer.renderCell(10, 0, ['value1', 'value2']); + const html = renderer.renderCell(10, 0, ['value1', 'value2']); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, 1); + const html = renderer.renderCell(11, 0, 1); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, '1'); + const html = renderer.renderCell(11, 0, '1'); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, 0); + const html = renderer.renderCell(11, 0, 0); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, '0'); + const html = renderer.renderCell(11, 0, '0'); expect(html).toBe(''); }); it('value should be mapped to text and colored cell should have style', () => { - var html = renderer.renderCell(11, 0, '2.1'); + const html = renderer.renderCell(11, 0, '2.1'); expect(html).toBe(''); }); it('value should be mapped to text (range) and colored cell should have style', () => { - var html = renderer.renderCell(12, 0, 0); + const html = renderer.renderCell(12, 0, 0); expect(html).toBe(''); }); it('value should be mapped to text (range) and colored cell should have style', () => { - var html = renderer.renderCell(12, 0, 1); + const html = renderer.renderCell(12, 0, 1); expect(html).toBe(''); }); it('value should be mapped to text (range) and colored cell should have style', () => { - var html = renderer.renderCell(12, 0, 4); + const html = renderer.renderCell(12, 0, 4); expect(html).toBe(''); }); it('value should be mapped to text (range) and colored cell should have style', () => { - var html = renderer.renderCell(12, 0, '7.1'); + const html = renderer.renderCell(12, 0, '7.1'); expect(html).toBe(''); }); }); diff --git a/public/app/plugins/panel/table/specs/transformers.test.ts b/public/app/plugins/panel/table/specs/transformers.test.ts index eefe3f9bdc0..2425d98f26d 100644 --- a/public/app/plugins/panel/table/specs/transformers.test.ts +++ b/public/app/plugins/panel/table/specs/transformers.test.ts @@ -1,11 +1,11 @@ import { transformers, transformDataToTable } from '../transformers'; describe('when transforming time series table', () => { - var table; + let table; describe('given 2 time series', () => { - var time = new Date().getTime(); - var timeSeries = [ + const time = new Date().getTime(); + const timeSeries = [ { target: 'series1', datapoints: [[12.12, time], [14.44, time + 1]], @@ -17,7 +17,7 @@ describe('when transforming time series table', () => { ]; describe('timeseries_to_rows', () => { - var panel = { + const panel = { transform: 'timeseries_to_rows', sort: { col: 0, desc: true }, }; @@ -43,7 +43,7 @@ describe('when transforming time series table', () => { }); describe('timeseries_to_columns', () => { - var panel = { + const panel = { transform: 'timeseries_to_columns', }; @@ -70,7 +70,7 @@ describe('when transforming time series table', () => { }); describe('timeseries_aggregations', () => { - var panel = { + const panel = { transform: 'timeseries_aggregations', sort: { col: 0, desc: true }, columns: [{ text: 'Max', value: 'max' }, { text: 'Min', value: 'min' }], @@ -99,12 +99,12 @@ describe('when transforming time series table', () => { describe('table data sets', () => { describe('Table', () => { const transform = 'table'; - var panel = { + const panel = { transform, }; - var time = new Date().getTime(); + const time = new Date().getTime(); - var nonTableData = [ + const nonTableData = [ { type: 'foo', columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value' }], @@ -112,7 +112,7 @@ describe('when transforming time series table', () => { }, ]; - var singleQueryData = [ + const singleQueryData = [ { type: 'table', columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value' }], @@ -120,7 +120,7 @@ describe('when transforming time series table', () => { }, ]; - var multipleQueriesDataSameLabels = [ + const multipleQueriesDataSameLabels = [ { type: 'table', columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #A' }], @@ -143,7 +143,7 @@ describe('when transforming time series table', () => { }, ]; - var multipleQueriesDataDifferentLabels = [ + const multipleQueriesDataDifferentLabels = [ { type: 'table', columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #A' }], @@ -163,14 +163,14 @@ describe('when transforming time series table', () => { describe('getColumns', function() { it('should return data columns given a single query', function() { - var columns = transformers[transform].getColumns(singleQueryData); + const columns = transformers[transform].getColumns(singleQueryData); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); expect(columns[2].text).toBe('Value'); }); it('should return the union of data columns given a multiple queries', function() { - var columns = transformers[transform].getColumns(multipleQueriesDataSameLabels); + const columns = transformers[transform].getColumns(multipleQueriesDataSameLabels); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); expect(columns[2].text).toBe('Label Key 2'); @@ -179,7 +179,7 @@ describe('when transforming time series table', () => { }); it('should return the union of data columns given a multiple queries with different labels', function() { - var columns = transformers[transform].getColumns(multipleQueriesDataDifferentLabels); + const columns = transformers[transform].getColumns(multipleQueriesDataDifferentLabels); expect(columns[0].text).toBe('Time'); expect(columns[1].text).toBe('Label Key 1'); expect(columns[2].text).toBe('Value #A'); @@ -263,7 +263,7 @@ describe('when transforming time series table', () => { describe('doc data sets', () => { describe('JSON Data', () => { - var panel = { + const panel = { transform: 'json', columns: [ { text: 'Timestamp', value: 'timestamp' }, @@ -271,7 +271,7 @@ describe('when transforming time series table', () => { { text: 'nested.level2', value: 'nested.level2' }, ], }; - var rawData = [ + const rawData = [ { type: 'docs', datapoints: [ @@ -288,7 +288,7 @@ describe('when transforming time series table', () => { describe('getColumns', function() { it('should return nested properties', function() { - var columns = transformers['json'].getColumns(rawData); + const columns = transformers['json'].getColumns(rawData); expect(columns[0].text).toBe('timestamp'); expect(columns[1].text).toBe('message'); expect(columns[2].text).toBe('nested.level2'); @@ -319,8 +319,8 @@ describe('when transforming time series table', () => { describe('annotation data', () => { describe('Annnotations', () => { - var panel = { transform: 'annotations' }; - var rawData = { + const panel = { transform: 'annotations' }; + const rawData = { annotations: [ { time: 1000, diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index 1608f890315..fed65097ac7 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -18,5 +18,5 @@ jest.mock('app/features/plugins/plugin_loader', () => ({})); configure({ adapter: new Adapter() }); -var global = window; +const global = window; global.$ = global.jQuery = $; diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 677419f3f75..960ce84f494 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -5,7 +5,7 @@ import { angularMocks, sinon } from '../lib/common'; import { PanelModel } from 'app/features/dashboard/panel_model'; export function ControllerTestContext() { - var self = this; + const self = this; this.datasource = {}; this.$element = {}; @@ -58,7 +58,7 @@ export function ControllerTestContext() { $rootScope.onAppEvent = sinon.spy(); $rootScope.colors = []; - for (var i = 0; i < 50; i++) { + for (let i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); } @@ -88,7 +88,7 @@ export function ControllerTestContext() { self.scope.onAppEvent = sinon.spy(); $rootScope.colors = []; - for (var i = 0; i < 50; i++) { + for (let i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); } @@ -107,7 +107,7 @@ export function ControllerTestContext() { } export function ServiceTestContext() { - var self = this; + const self = this; self.templateSrv = new TemplateSrvStub(); self.timeSrv = new TimeSrvStub(); self.datasourceSrv = {}; @@ -195,7 +195,7 @@ export function TemplateSrvStub() { }; } -var allDeps = { +const allDeps = { ContextSrvStub, TemplateSrvStub, TimeSrvStub, From 35c00891e722cc68dfb9cefe8537d5229661754c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 26 Aug 2018 20:19:23 +0200 Subject: [PATCH 409/410] tslint: more const fixes (#13035) --- public/app/core/config.ts | 6 ++-- .../features/admin/admin_edit_user_ctrl.ts | 6 ++-- .../alerting/notification_edit_ctrl.ts | 2 +- .../annotations/specs/annotations_srv.test.ts | 6 +--- .../app/features/dashboard/ad_hoc_filters.ts | 12 +++---- .../app/features/dashboard/change_tracker.ts | 18 +++++----- .../dashboard/dashboard_import_ctrl.ts | 12 +++---- .../dashboard/dashgrid/AddPanelPanel.tsx | 2 +- .../features/dashboard/export/export_modal.ts | 4 +-- .../app/features/dashboard/export/exporter.ts | 22 ++++++------ .../dashboard/repeat_option/repeat_option.ts | 2 +- .../features/dashboard/settings/settings.ts | 2 +- .../dashboard/specs/change_tracker.test.ts | 2 +- .../specs/dashboard_import_ctrl.test.ts | 2 +- .../specs/dashboard_migration.test.ts | 11 +++--- public/app/features/dashboard/time_srv.ts | 34 +++++++++---------- .../dashboard/timepicker/input_date.ts | 6 ++-- .../dashboard/timepicker/timepicker.ts | 10 +++--- public/app/features/dashboard/upload.ts | 10 +++--- public/app/features/org/org_api_keys_ctrl.ts | 2 +- public/app/features/org/org_details_ctrl.ts | 2 +- public/app/features/org/prefs_control.ts | 4 +-- public/app/features/panel/panel_directive.ts | 26 +++++++------- public/app/features/panel/panel_editor_tab.ts | 8 ++--- public/app/features/panel/panel_header.ts | 2 +- public/app/features/panel/query_editor_row.ts | 6 ++-- .../features/panel/query_troubleshooter.ts | 2 +- public/app/features/panel/solo_panel_ctrl.ts | 4 +-- .../features/playlist/playlist_edit_ctrl.ts | 8 ++--- .../app/features/playlist/playlist_search.ts | 4 +-- public/app/features/playlist/playlist_srv.ts | 4 +-- public/app/features/styleguide/styleguide.ts | 2 +- 32 files changed, 120 insertions(+), 123 deletions(-) diff --git a/public/app/core/config.ts b/public/app/core/config.ts index e065ddb22fb..f522c6340e6 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -31,7 +31,7 @@ export class Settings { loginError: any; constructor(options) { - var defaults = { + const defaults = { datasources: {}, window_title_prefix: 'Grafana - ', panels: {}, @@ -51,8 +51,8 @@ export class Settings { } } -var bootData = (window).grafanaBootData || { settings: {} }; -var options = bootData.settings; +const bootData = (window).grafanaBootData || { settings: {} }; +const options = bootData.settings; options.bootData = bootData; const config = new Settings(options); diff --git a/public/app/features/admin/admin_edit_user_ctrl.ts b/public/app/features/admin/admin_edit_user_ctrl.ts index 1d4fb9cf19a..b84b690d44a 100644 --- a/public/app/features/admin/admin_edit_user_ctrl.ts +++ b/public/app/features/admin/admin_edit_user_ctrl.ts @@ -29,14 +29,14 @@ export class AdminEditUserCtrl { return; } - var payload = { password: $scope.password }; + const payload = { password: $scope.password }; backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(function() { $location.path('/admin/users'); }); }; $scope.updatePermissions = function() { - var payload = $scope.permissions; + const payload = $scope.permissions; backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(function() { $location.path('/admin/users'); @@ -99,7 +99,7 @@ export class AdminEditUserCtrl { return; } - var orgInfo = _.find($scope.orgsSearchCache, { + const orgInfo = _.find($scope.orgsSearchCache, { name: $scope.newOrg.name, }); if (!orgInfo) { diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index eb14766d1fb..60942e6ffb4 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -99,7 +99,7 @@ export class AlertNotificationEditCtrl { return; } - var payload = { + const payload = { name: this.model.name, type: this.model.type, settings: this.model.settings, diff --git a/public/app/features/annotations/specs/annotations_srv.test.ts b/public/app/features/annotations/specs/annotations_srv.test.ts index 97696767536..f262544da43 100644 --- a/public/app/features/annotations/specs/annotations_srv.test.ts +++ b/public/app/features/annotations/specs/annotations_srv.test.ts @@ -6,12 +6,8 @@ describe('AnnotationsSrv', function() { const $rootScope = { onAppEvent: jest.fn(), }; - let $q; - let datasourceSrv; - let backendSrv; - let timeSrv; - const annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); + const annotationsSrv = new AnnotationsSrv($rootScope, null, null, null, null); describe('When translating the query result', () => { const annotationSource = { diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index 412761dc716..68b068152b5 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -55,8 +55,8 @@ export class AdHocFiltersCtrl { } return this.datasourceSrv.get(this.variable.datasource).then(ds => { - var options: any = {}; - var promise = null; + const options: any = {}; + let promise = null; if (segment.type !== 'value') { promise = ds.getTagKeys(); @@ -113,9 +113,9 @@ export class AdHocFiltersCtrl { } updateVariableModel() { - var filters = []; - var filterIndex = -1; - var hasFakes = false; + const filters = []; + let filterIndex = -1; + let hasFakes = false; this.segments.forEach(segment => { if (segment.type === 'value' && segment.fake) { @@ -153,7 +153,7 @@ export class AdHocFiltersCtrl { } } -var template = ` +const template = `
    { + const self = this; + const cancel = this.$rootScope.$on('dashboard-saved', () => { cancel(); this.$timeout(() => { self.gotoNext(); @@ -179,8 +179,8 @@ export class ChangeTracker { } gotoNext() { - var baseLen = this.$location.absUrl().length - this.$location.url().length; - var nextUrl = this.next.substring(baseLen); + const baseLen = this.$location.absUrl().length - this.$location.url().length; + const nextUrl = this.next.substring(baseLen); this.$location.url(nextUrl); } } diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index b70a1847602..3dfae1250dd 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -52,7 +52,7 @@ export class DashboardImportCtrl { if (this.dash.__inputs) { for (const input of this.dash.__inputs) { - var inputModel = { + const inputModel = { name: input.name, label: input.label, info: input.description, @@ -78,7 +78,7 @@ export class DashboardImportCtrl { } setDatasourceOptions(input, inputModel) { - var sources = _.filter(config.datasources, val => { + const sources = _.filter(config.datasources, val => { return val.type === input.pluginId; }); @@ -162,7 +162,7 @@ export class DashboardImportCtrl { } saveDashboard() { - var inputs = this.inputs.map(input => { + const inputs = this.inputs.map(input => { return { name: input.name, type: input.type, @@ -186,7 +186,7 @@ export class DashboardImportCtrl { loadJsonText() { try { this.parseError = ''; - var dash = JSON.parse(this.jsonText); + const dash = JSON.parse(this.jsonText); this.onUpload(dash); } catch (err) { console.log(err); @@ -198,8 +198,8 @@ export class DashboardImportCtrl { checkGnetDashboard() { this.gnetError = ''; - var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); - var dashboardId; + const match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); + let dashboardId; if (match && match[1]) { dashboardId = match[1]; diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 9459fc41753..a26a0401d56 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -97,7 +97,7 @@ export class AddPanelPanel extends React.Component { + const templateizeDatasourceUsage = obj => { // ignore data source properties that contain a variable if (obj.datasource && obj.datasource.indexOf('$') === 0) { if (variableLookup[obj.datasource.substring(1)]) { @@ -42,7 +42,7 @@ export class DashboardExporter { return; } - var refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); + const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); datasources[refName] = { name: refName, label: ds.name, @@ -76,7 +76,7 @@ export class DashboardExporter { } } - var panelDef = config.panels[panel.type]; + const panelDef = config.panels[panel.type]; if (panelDef) { requires['panel' + panelDef.id] = { type: 'panel', @@ -131,7 +131,7 @@ export class DashboardExporter { // templatize constants for (const variable of saveModel.templating.list) { if (variable.type === 'constant') { - var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); + const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); inputs.push({ name: refName, type: 'constant', @@ -149,7 +149,7 @@ export class DashboardExporter { } // make inputs and requires a top thing - var newObj = {}; + const newObj = {}; newObj['__inputs'] = inputs; newObj['__requires'] = _.sortBy(requires, ['id']); diff --git a/public/app/features/dashboard/repeat_option/repeat_option.ts b/public/app/features/dashboard/repeat_option/repeat_option.ts index 696c634ddae..01e1d716fc5 100644 --- a/public/app/features/dashboard/repeat_option/repeat_option.ts +++ b/public/app/features/dashboard/repeat_option/repeat_option.ts @@ -1,6 +1,6 @@ import { coreModule } from 'app/core/core'; -var template = ` +const template = `
    2014-01-01T06:06:06Z--1.23 kbps1.230 sasd40.055.085.0value&breaking <br /> the <br /> row&breaking <br /> the <br /> rowsanitizedvalue1, value2onoffHELLO GRAFANAvalue3, value4onoffvalue1, value2ononoffoff2.10onoff7.1