diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index 50f18ea6146..937f3c6cd9d 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -3,11 +3,19 @@
Value
-
+
+
+
+ +
+ +
+
+
diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index c4d7ed06aa4..406aedb3877 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -14,6 +14,7 @@ import {MetricsPanelCtrl} from 'app/plugins/sdk'; class SingleStatCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; + dataType = 'timeseries'; series: any[]; data: any; fontSizes: any[]; @@ -22,6 +23,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { panel: any; events: any; valueNameOptions: any[] = ['min','max','avg', 'current', 'total', 'name', 'first', 'delta', 'diff', 'range']; + tableColumnOptions: any; // Set and populate defaults panelDefaults = { @@ -67,7 +69,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { maxValue: 100, thresholdMarkers: true, thresholdLabels: false - } + }, + tableColumn: '' }; /** @ngInject */ @@ -98,11 +101,16 @@ class SingleStatCtrl extends MetricsPanelCtrl { } onDataReceived(dataList) { - this.series = dataList.map(this.seriesHandler.bind(this)); - - var data: any = {}; - this.setValues(data); - + const data: any = {}; + if (dataList.length > 0 && dataList[0].type === 'table'){ + this.dataType = 'table'; + const tableData = dataList.map(this.tableHandler.bind(this)); + this.setTableValues(tableData, data); + } else { + this.dataType = 'timeseries'; + this.series = dataList.map(this.seriesHandler.bind(this)); + this.setValues(data); + } this.data = data; this.render(); } @@ -117,6 +125,69 @@ class SingleStatCtrl extends MetricsPanelCtrl { return series; } + tableHandler(tableData) { + const datapoints = []; + const columnNames = {}; + + tableData.columns.forEach((column, columnIndex) => { + columnNames[columnIndex] = column.text; + }); + + this.tableColumnOptions = columnNames; + if (!_.find(tableData.columns, ['text', this.panel.tableColumn])) { + this.setTableColumnToSensibleDefault(tableData); + } + + tableData.rows.forEach((row) => { + const datapoint = {}; + + row.forEach((value, columnIndex) => { + const key = columnNames[columnIndex]; + datapoint[key] = value; + }); + + datapoints.push(datapoint); + }); + + return datapoints; + } + + setTableColumnToSensibleDefault(tableData) { + if (this.tableColumnOptions.length === 1) { + this.panel.tableColumn = this.tableColumnOptions[0]; + } else { + this.panel.tableColumn = _.find(tableData.columns, (col) => { return col.type !== 'time'; }).text; + } + } + + setTableValues(tableData, data) { + if (!tableData || tableData.length === 0) { + return; + } + + if (tableData[0].length === 0 || !tableData[0][0][this.panel.tableColumn]) { + return; + } + + let highestValue = 0; + let lowestValue = Number.MAX_VALUE; + const datapoint = tableData[0][0]; + data.value = datapoint[this.panel.tableColumn]; + + if (_.isString(data.value)) { + data.valueFormatted = _.escape(data.value); + data.value = 0; + data.valueRounded = 0; + } else { + const decimalInfo = this.getDecimalsForValue(data.value); + const formatFunc = kbn.valueFormats[this.panel.format]; + data.valueFormatted = formatFunc(datapoint[this.panel.tableColumn], decimalInfo.decimals, decimalInfo.scaledDecimals); + data.valueRounded = kbn.roundValue(data.value, this.panel.decimals || 0); + } + + this.setValueMapping(data); + } + setColoring(options) { if (options.background) { this.panel.colorValue = false; @@ -192,10 +263,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { if (this.panel.valueName === 'name') { data.value = 0; data.valueRounded = 0; - data.valueFormated = this.series[0].alias; + data.valueFormatted = this.series[0].alias; } else if (_.isString(lastValue)) { data.value = 0; - data.valueFormated = _.escape(lastValue); + data.valueFormatted = _.escape(lastValue); data.valueRounded = 0; } else { data.value = this.series[0].stats[this.panel.valueName]; @@ -203,7 +274,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { var decimalInfo = this.getDecimalsForValue(data.value); var formatFunc = kbn.valueFormats[this.panel.format]; - data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); + data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals); data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals); } @@ -211,7 +282,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { data.scopedVars = _.extend({}, this.panel.scopedVars); data.scopedVars["__name"] = {value: this.series[0].label}; } + this.setValueMapping(data); + } + setValueMapping(data) { // check value to text mappings if its enabled if (this.panel.mappingType === 1) { for (let i = 0; i < this.panel.valueMaps.length; i++) { @@ -219,7 +293,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { // special null case if (map.value === 'null') { if (data.value === null || data.value === void 0) { - data.valueFormated = map.text; + data.valueFormatted = map.text; return; } continue; @@ -228,7 +302,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { // value/number to text mapping var value = parseFloat(map.value); if (value === data.valueRounded) { - data.valueFormated = map.text; + data.valueFormatted = map.text; return; } } @@ -238,7 +312,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { // special null case if (map.from === 'null' && map.to === 'null') { if (data.value === null || data.value === void 0) { - data.valueFormated = map.text; + data.valueFormatted = map.text; return; } continue; @@ -248,14 +322,14 @@ class SingleStatCtrl extends MetricsPanelCtrl { var from = parseFloat(map.from); var to = parseFloat(map.to); if (to >= data.valueRounded && from <= data.valueRounded) { - data.valueFormated = map.text; + data.valueFormatted = map.text; return; } } } if (data.value === null || data.value === void 0) { - data.valueFormated = "no value"; + data.valueFormatted = "no value"; } } @@ -317,7 +391,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { if (panel.prefix) { body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, panel.prefix); } - var value = applyColoringThresholds(data.value, data.valueFormated); + var value = applyColoringThresholds(data.value, data.valueFormatted); body += getSpan('singlestat-panel-value', panel.valueFontSize, value); if (panel.postfix) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); } @@ -329,7 +403,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { function getValueText() { var result = panel.prefix ? panel.prefix : ''; - result += data.valueFormated; + result += data.valueFormatted; result += panel.postfix ? panel.postfix : ''; return result; 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 3f67063bc0f..00000000000 --- a/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts +++ /dev/null @@ -1,131 +0,0 @@ -/// - -import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common'; - -import angular from 'angular'; -import helpers from '../../../../../test/specs/helpers'; -import {SingleStatCtrl} from '../module'; - -describe('SingleStatCtrl', function() { - var ctx = new helpers.ControllerTestContext(); - - 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(); - var data = [ - {target: 'test.cpu1', datapoints: ctx.datapoints} - ]; - - ctx.ctrl.onDataReceived(data); - ctx.data = ctx.ctrl.data; - }); - }; - - func(ctx); - }); - } - - singleStatScenario('with defaults', function(ctx) { - ctx.setup(function() { - ctx.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 formated falue', function() { - expect(ctx.data.valueFormated).to.be('15'); - }); - }); - - singleStatScenario('showing serie name instead of value', function(ctx) { - ctx.setup(function() { - ctx.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 formated falue', function() { - expect(ctx.data.valueFormated).to.be('test.cpu1'); - }); - }); - - singleStatScenario('MainValue should use same number for decimals as displayed when checking thresholds', function(ctx) { - ctx.setup(function() { - ctx.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 formated falue', function() { - expect(ctx.data.valueFormated).to.be('100'); - }); - }); - - singleStatScenario('When value to text mapping is specified', function(ctx) { - ctx.setup(function() { - ctx.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.valueFormated).to.be('OK'); - }); - }); - - singleStatScenario('When range to text mapping is specifiedfor first range', function(ctx) { - ctx.setup(function() { - ctx.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.valueFormated).to.be('OK'); - }); - }); - - singleStatScenario('When range to text mapping is specified for other ranges', function(ctx) { - ctx.setup(function() { - ctx.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.valueFormated).to.be('NOT OK'); - }); - }); - -}); diff --git a/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts b/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts new file mode 100644 index 00000000000..6e05266b8fe --- /dev/null +++ b/public/app/plugins/panel/singlestat/specs/singlestat_specs.ts @@ -0,0 +1,260 @@ +/// + +import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common'; + +import angular from 'angular'; +import helpers from '../../../../../test/specs/helpers'; +import {SingleStatCtrl} from '../module'; + +describe('SingleStatCtrl', function() { + var ctx = new helpers.ControllerTestContext(); + + 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 falue', function() { + expect(ctx.data.valueFormatted).to.be('test.cpu1'); + }); + }); + + 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 falue', 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 specifiedfor 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'); + }); + }); + }); +}); + +