From 1d9ad9be3367de8989ab600244586be9fcffd561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Jan 2016 17:20:50 +0100 Subject: [PATCH] feat(plugins): upgraded singlestat to new plugin format --- public/app/partials/dashboard.html | 2 + .../plugins/panel/singlestat/controller.js | 246 ++++++++++ .../app/plugins/panel/singlestat/module.html | 2 +- public/app/plugins/panel/singlestat/module.js | 428 +++++++++--------- public/test/specs/singlestat-specs.js | 11 +- 5 files changed, 459 insertions(+), 230 deletions(-) create mode 100644 public/app/plugins/panel/singlestat/controller.js diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 3234903ff8c..df8e1f5f82b 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -10,6 +10,8 @@
+ +
diff --git a/public/app/plugins/panel/singlestat/controller.js b/public/app/plugins/panel/singlestat/controller.js new file mode 100644 index 00000000000..357bffb8fa2 --- /dev/null +++ b/public/app/plugins/panel/singlestat/controller.js @@ -0,0 +1,246 @@ +define([ + 'angular', + 'app/app', + 'lodash', + 'app/core/utils/kbn', + 'app/core/time_series', + 'app/features/panel/panel_meta', +], +function (angular, app, _, kbn, TimeSeries, PanelMeta) { + 'use strict'; + + /** @ngInject */ + function SingleStatCtrl($scope, panelSrv, panelHelper) { + console.log('controller'); + + $scope.panelMeta = new PanelMeta({ + panelName: 'Singlestat', + editIcon: "fa fa-dashboard", + fullscreen: true, + metricsEditor: true + }); + + $scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%']; + + $scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/singlestat/editor.html'); + $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html'); + + // Set and populate defaults + var _d = { + links: [], + datasource: null, + maxDataPoints: 100, + interval: null, + targets: [{}], + cacheTimeout: null, + format: 'none', + prefix: '', + postfix: '', + nullText: null, + valueMaps: [ + { value: 'null', op: '=', text: 'N/A' } + ], + nullPointMode: 'connected', + valueName: 'avg', + prefixFontSize: '50%', + valueFontSize: '80%', + postfixFontSize: '50%', + thresholds: '', + colorBackground: false, + colorValue: false, + colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + sparkline: { + show: false, + full: false, + lineColor: 'rgb(31, 120, 193)', + fillColor: 'rgba(31, 118, 189, 0.18)', + } + }; + + _.defaults($scope.panel, _d); + $scope.unitFormats = kbn.getUnitFormats(); + + $scope.setUnitFormat = function(subItem) { + $scope.panel.format = subItem.value; + $scope.render(); + }; + + $scope.init = function() { + panelSrv.init($scope); + }; + + $scope.refreshData = function(datasource) { + panelHelper.updateTimeRange($scope); + + return panelHelper.issueMetricQuery($scope, datasource) + .then($scope.dataHandler, function(err) { + $scope.series = []; + $scope.render(); + throw err; + }); + }; + + $scope.loadSnapshot = function(snapshotData) { + panelHelper.updateTimeRange($scope); + $scope.dataHandler(snapshotData); + }; + + $scope.dataHandler = function(results) { + $scope.series = _.map(results.data, $scope.seriesHandler); + $scope.render(); + }; + + $scope.seriesHandler = function(seriesData) { + var series = new TimeSeries({ + datapoints: seriesData.datapoints, + alias: seriesData.target, + }); + + series.flotpairs = series.getFlotPairs($scope.panel.nullPointMode); + + return series; + }; + + $scope.setColoring = function(options) { + if (options.background) { + $scope.panel.colorValue = false; + $scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)']; + } + else { + $scope.panel.colorBackground = false; + $scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)']; + } + $scope.render(); + }; + + $scope.invertColorOrder = function() { + var tmp = $scope.panel.colors[0]; + $scope.panel.colors[0] = $scope.panel.colors[2]; + $scope.panel.colors[2] = tmp; + $scope.render(); + }; + + $scope.getDecimalsForValue = function(value) { + if (_.isNumber($scope.panel.decimals)) { + return { decimals: $scope.panel.decimals, scaledDecimals: null }; + } + + var delta = value / 2; + var dec = -Math.floor(Math.log(delta) / Math.LN10); + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + // reduce starting decimals if not needed + if (Math.floor(value) === value) { dec = 0; } + + var result = {}; + result.decimals = Math.max(0, dec); + result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2; + + return result; + }; + + $scope.render = function() { + var data = {}; + + $scope.setValues(data); + + data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) { + return Number(strVale.trim()); + }); + + data.colorMap = $scope.panel.colors; + + $scope.data = data; + $scope.$broadcast('render'); + }; + + $scope.setValues = function(data) { + data.flotpairs = []; + + if($scope.series.length > 1) { + $scope.inspector.error = new Error(); + $scope.inspector.error.message = 'Multiple Series Error'; + $scope.inspector.error.data = 'Metric query returns ' + $scope.series.length + + ' series. Single Stat Panel expects a single series.\n\nResponse:\n'+JSON.stringify($scope.series); + throw $scope.inspector.error; + } + + if ($scope.series && $scope.series.length > 0) { + var lastPoint = _.last($scope.series[0].datapoints); + var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null; + + if (_.isString(lastValue)) { + data.value = 0; + data.valueFormated = lastValue; + data.valueRounded = 0; + } else { + data.value = $scope.series[0].stats[$scope.panel.valueName]; + data.flotpairs = $scope.series[0].flotpairs; + + var decimalInfo = $scope.getDecimalsForValue(data.value); + var formatFunc = kbn.valueFormats[$scope.panel.format]; + data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); + data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); + } + } + + // check value to text mappings + for(var i = 0; i < $scope.panel.valueMaps.length; i++) { + var map = $scope.panel.valueMaps[i]; + // special null case + if (map.value === 'null') { + if (data.value === null || data.value === void 0) { + data.valueFormated = map.text; + return; + } + continue; + } + + // value/number to text mapping + var value = parseFloat(map.value); + if (value === data.value) { + data.valueFormated = map.text; + return; + } + } + + if (data.value === null || data.value === void 0) { + data.valueFormated = "no value"; + } + }; + + $scope.removeValueMap = function(map) { + var index = _.indexOf($scope.panel.valueMaps, map); + $scope.panel.valueMaps.splice(index, 1); + $scope.render(); + }; + + $scope.addValueMap = function() { + $scope.panel.valueMaps.push({value: '', op: '=', text: '' }); + }; + + $scope.init(); + } + + return SingleStatCtrl; +}); diff --git a/public/app/plugins/panel/singlestat/module.html b/public/app/plugins/panel/singlestat/module.html index 5aeb468db27..748915e6329 100644 --- a/public/app/plugins/panel/singlestat/module.html +++ b/public/app/plugins/panel/singlestat/module.html @@ -1,4 +1,4 @@ -
+
diff --git a/public/app/plugins/panel/singlestat/module.js b/public/app/plugins/panel/singlestat/module.js index 5c99cf7d3b1..2ed8221150a 100644 --- a/public/app/plugins/panel/singlestat/module.js +++ b/public/app/plugins/panel/singlestat/module.js @@ -1,253 +1,231 @@ define([ - 'angular', - 'app/app', + './controller', 'lodash', - 'app/core/utils/kbn', - 'app/core/time_series', - 'app/features/panel/panel_meta', - './singleStatPanel', + 'jquery', + 'jquery.flot', ], -function (angular, app, _, kbn, TimeSeries, PanelMeta) { +function (SingleStatCtrl, _, $) { 'use strict'; - var module = angular.module('grafana.panels.singlestat'); - app.useModule(module); - - module.directive('grafanaPanelSinglestat', function() { + /** @ngInject */ + function singleStatPanel($location, linkSrv, $timeout, templateSrv) { return { - controller: 'SingleStatCtrl', + controller: SingleStatCtrl, templateUrl: 'app/plugins/panel/singlestat/module.html', - }; - }); + link: function(scope, elem) { + var data, panel, linkInfo, $panelContainer; + var firstRender = true; - module.controller('SingleStatCtrl', function($scope, panelSrv, panelHelper) { + scope.$on('render', function() { + if (firstRender) { + var inner = elem.find('.singlestat-panel'); + if (inner.length) { + elem = inner; + $panelContainer = elem.parents('.panel-container'); + firstRender = false; + } + } - $scope.panelMeta = new PanelMeta({ - panelName: 'Singlestat', - editIcon: "fa fa-dashboard", - fullscreen: true, - metricsEditor: true - }); - - $scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%']; - - $scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/singlestat/editor.html'); - $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html'); - - // Set and populate defaults - var _d = { - links: [], - datasource: null, - maxDataPoints: 100, - interval: null, - targets: [{}], - cacheTimeout: null, - format: 'none', - prefix: '', - postfix: '', - nullText: null, - valueMaps: [ - { value: 'null', op: '=', text: 'N/A' } - ], - nullPointMode: 'connected', - valueName: 'avg', - prefixFontSize: '50%', - valueFontSize: '80%', - postfixFontSize: '50%', - thresholds: '', - colorBackground: false, - colorValue: false, - colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], - sparkline: { - show: false, - full: false, - lineColor: 'rgb(31, 120, 193)', - fillColor: 'rgba(31, 118, 189, 0.18)', - } - }; - - _.defaults($scope.panel, _d); - $scope.unitFormats = kbn.getUnitFormats(); - - $scope.setUnitFormat = function(subItem) { - $scope.panel.format = subItem.value; - $scope.render(); - }; - - $scope.init = function() { - panelSrv.init($scope); - }; - - $scope.refreshData = function(datasource) { - panelHelper.updateTimeRange($scope); - - return panelHelper.issueMetricQuery($scope, datasource) - .then($scope.dataHandler, function(err) { - $scope.series = []; - $scope.render(); - throw err; + render(); + scope.panelRenderingComplete(); }); - }; - $scope.loadSnapshot = function(snapshotData) { - panelHelper.updateTimeRange($scope); - $scope.dataHandler(snapshotData); - }; + function setElementHeight() { + try { + var height = scope.height || panel.height || scope.row.height; + if (_.isString(height)) { + height = parseInt(height.replace('px', ''), 10); + } - $scope.dataHandler = function(results) { - $scope.series = _.map(results.data, $scope.seriesHandler); - $scope.render(); - }; + height -= 5; // padding + height -= panel.title ? 24 : 9; // subtract panel title bar - $scope.seriesHandler = function(seriesData) { - var series = new TimeSeries({ - datapoints: seriesData.datapoints, - alias: seriesData.target, - }); + elem.css('height', height + 'px'); - series.flotpairs = series.getFlotPairs($scope.panel.nullPointMode); - - return series; - }; - - $scope.setColoring = function(options) { - if (options.background) { - $scope.panel.colorValue = false; - $scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)']; - } - else { - $scope.panel.colorBackground = false; - $scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)']; - } - $scope.render(); - }; - - $scope.invertColorOrder = function() { - var tmp = $scope.panel.colors[0]; - $scope.panel.colors[0] = $scope.panel.colors[2]; - $scope.panel.colors[2] = tmp; - $scope.render(); - }; - - $scope.getDecimalsForValue = function(value) { - if (_.isNumber($scope.panel.decimals)) { - return { decimals: $scope.panel.decimals, scaledDecimals: null }; - } - - var delta = value / 2; - var dec = -Math.floor(Math.log(delta) / Math.LN10); - - var magn = Math.pow(10, -dec), - norm = delta / magn, // norm is between 1.0 and 10.0 - size; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25) { - size = 2.5; - ++dec; + return true; + } catch(e) { // IE throws errors sometimes + return false; + } } - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - size *= magn; + function applyColoringThresholds(value, valueString) { + if (!panel.colorValue) { + return valueString; + } - // reduce starting decimals if not needed - if (Math.floor(value) === value) { dec = 0; } + var color = getColorForValue(value); + if (color) { + return ''+ valueString + ''; + } - var result = {}; - result.decimals = Math.max(0, dec); - result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2; - - return result; - }; - - $scope.render = function() { - var data = {}; - - $scope.setValues(data); - - data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) { - return Number(strVale.trim()); - }); - - data.colorMap = $scope.panel.colors; - - $scope.data = data; - $scope.$broadcast('render'); - }; - - $scope.setValues = function(data) { - data.flotpairs = []; - - if($scope.series.length > 1) { - $scope.inspector.error = new Error(); - $scope.inspector.error.message = 'Multiple Series Error'; - $scope.inspector.error.data = 'Metric query returns ' + $scope.series.length + - ' series. Single Stat Panel expects a single series.\n\nResponse:\n'+JSON.stringify($scope.series); - throw $scope.inspector.error; - } - - if ($scope.series && $scope.series.length > 0) { - var lastPoint = _.last($scope.series[0].datapoints); - var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null; - - if (_.isString(lastValue)) { - data.value = 0; - data.valueFormated = lastValue; - data.valueRounded = 0; - } else { - data.value = $scope.series[0].stats[$scope.panel.valueName]; - data.flotpairs = $scope.series[0].flotpairs; - - var decimalInfo = $scope.getDecimalsForValue(data.value); - var formatFunc = kbn.valueFormats[$scope.panel.format]; - data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); - data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); + return valueString; } - } - // check value to text mappings - for(var i = 0; i < $scope.panel.valueMaps.length; i++) { - var map = $scope.panel.valueMaps[i]; - // special null case - if (map.value === 'null') { - if (data.value === null || data.value === void 0) { - data.valueFormated = map.text; + function getColorForValue(value) { + for (var i = data.thresholds.length - 1; i >= 0 ; i--) { + if (value >= data.thresholds[i]) { + return data.colorMap[i]; + } + } + return null; + } + + function getSpan(className, fontSize, value) { + value = templateSrv.replace(value); + return '' + + value + ''; + } + + function getBigValueHtml() { + var body = '
'; + + if (panel.prefix) { body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, scope.panel.prefix); } + + var value = applyColoringThresholds(data.valueRounded, data.valueFormated); + body += getSpan('singlestat-panel-value', panel.valueFontSize, value); + + if (panel.postfix) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); } + + body += '
'; + + return body; + } + + function addSparkline() { + var panel = scope.panel; + var width = elem.width() + 20; + var height = elem.height() || 100; + + var plotCanvas = $('
'); + var plotCss = {}; + plotCss.position = 'absolute'; + + if (panel.sparkline.full) { + plotCss.bottom = '5px'; + plotCss.left = '-5px'; + plotCss.width = (width - 10) + 'px'; + var dynamicHeightMargin = height <= 100 ? 5 : (Math.round((height/100)) * 15) + 5; + plotCss.height = (height - dynamicHeightMargin) + 'px'; + } + else { + plotCss.bottom = "0px"; + plotCss.left = "-5px"; + plotCss.width = (width - 10) + 'px'; + plotCss.height = Math.floor(height * 0.25) + "px"; + } + + plotCanvas.css(plotCss); + + var options = { + legend: { show: false }, + series: { + lines: { + show: true, + fill: 1, + lineWidth: 1, + fillColor: panel.sparkline.fillColor, + }, + }, + yaxes: { show: false }, + xaxis: { + show: false, + mode: "time", + min: scope.range.from.valueOf(), + max: scope.range.to.valueOf(), + }, + grid: { hoverable: false, show: false }, + }; + + elem.append(plotCanvas); + + var plotSeries = { + data: data.flotpairs, + color: panel.sparkline.lineColor + }; + + $.plot(plotCanvas, [plotSeries], options); + } + + function render() { + if (!scope.data) { return; } + + data = scope.data; + panel = scope.panel; + + setElementHeight(); + + var body = getBigValueHtml(); + + if (panel.colorBackground && !isNaN(data.valueRounded)) { + var color = getColorForValue(data.valueRounded); + if (color) { + $panelContainer.css('background-color', color); + if (scope.fullscreen) { + elem.css('background-color', color); + } else { + elem.css('background-color', ''); + } + } + } else { + $panelContainer.css('background-color', ''); + elem.css('background-color', ''); + } + + elem.html(body); + + if (panel.sparkline.show) { + addSparkline(); + } + + elem.toggleClass('pointer', panel.links.length > 0); + + if (panel.links.length > 0) { + linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], scope.panel.scopedVars); + } else { + linkInfo = null; + } + } + + // drilldown link tooltip + var drilldownTooltip = $('
hello
"'); + + elem.mouseleave(function() { + if (panel.links.length === 0) { return;} + drilldownTooltip.detach(); + }); + + elem.click(function() { + if (!linkInfo) { return; } + + if (linkInfo.target === '_blank') { + var redirectWindow = window.open(linkInfo.href, '_blank'); + redirectWindow.location; return; } - continue; - } - // value/number to text mapping - var value = parseFloat(map.value); - if (value === data.value) { - data.valueFormated = map.text; - return; - } - } + if (linkInfo.href.indexOf('http') === 0) { + window.location.href = linkInfo.href; + } else { + $timeout(function() { + $location.url(linkInfo.href); + }); + } - if (data.value === null || data.value === void 0) { - data.valueFormated = "no value"; + drilldownTooltip.detach(); + }); + + elem.mousemove(function(e) { + if (!linkInfo) { return;} + + drilldownTooltip.text('click to go to: ' + linkInfo.title); + + drilldownTooltip.place_tt(e.pageX+20, e.pageY-15); + }); } }; + } - $scope.removeValueMap = function(map) { - var index = _.indexOf($scope.panel.valueMaps, map); - $scope.panel.valueMaps.splice(index, 1); - $scope.render(); - }; - - $scope.addValueMap = function() { - $scope.panel.valueMaps.push({value: '', op: '=', text: '' }); - }; - - $scope.init(); - }); + return { + panel: singleStatPanel + }; }); diff --git a/public/test/specs/singlestat-specs.js b/public/test/specs/singlestat-specs.js index 733f7f37e98..60953345f2d 100644 --- a/public/test/specs/singlestat-specs.js +++ b/public/test/specs/singlestat-specs.js @@ -1,11 +1,14 @@ define([ + 'angular', './helpers', + 'app/plugins/panel/singlestat/controller', 'app/features/panel/panel_srv', 'app/features/panel/panel_helper', - 'app/plugins/panel/singlestat/module' -], function(helpers) { +], function(angular, helpers, SingleStatCtrl) { 'use strict'; + angular.module('grafana.controllers').controller('SingleStatCtrl', SingleStatCtrl); + describe('SingleStatCtrl', function() { var ctx = new helpers.ControllerTestContext(); @@ -16,7 +19,7 @@ define([ ctx.setup = function (setupFunc) { beforeEach(module('grafana.services')); - beforeEach(module('grafana.panels.singlestat')); + beforeEach(module('grafana.controllers')); beforeEach(ctx.providePhase()); beforeEach(ctx.createControllerPhase('SingleStatCtrl')); @@ -24,7 +27,7 @@ define([ beforeEach(function() { setupFunc(); ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ { target: 'test.cpu1', datapoints: ctx.datapoints } ] + data: [{target: 'test.cpu1', datapoints: ctx.datapoints}] })); ctx.scope.refreshData(ctx.datasource);