Commit 68f74135 by Ryan McKinley Committed by GitHub

SingleStat: use DataFrame results rather than TimeSeries/TableData (#18580)

parent e6fbf358
...@@ -501,6 +501,237 @@ ...@@ -501,6 +501,237 @@
} }
], ],
"valueName": "current" "valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorPrefix": false,
"colorValue": false,
"colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
"datasource": "gdev-testdata",
"decimals": null,
"description": "",
"format": "none",
"gauge": {
"maxValue": 150,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
"w": 8,
"x": 0,
"y": 14
},
"id": 8,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"options": {},
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "Info",
"targets": [
{
"alias": "",
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "random_walk_table",
"stringInput": ""
}
],
"thresholds": "81,90",
"title": "TableData 'Info' string Column",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [],
"valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorPrefix": false,
"colorValue": false,
"colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
"datasource": "gdev-testdata",
"decimals": 2,
"description": "",
"format": "celsius",
"gauge": {
"maxValue": 150,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
"w": 8,
"x": 8,
"y": 14
},
"id": 9,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"options": {},
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "Min",
"targets": [
{
"alias": "",
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "random_walk_table",
"stringInput": ""
}
],
"thresholds": "81,90",
"title": "TableData 'Value' as temp Column",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [],
"valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorPrefix": false,
"colorValue": false,
"colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
"datasource": "gdev-testdata",
"decimals": null,
"description": "",
"format": "dateTimeFromNow",
"gauge": {
"maxValue": 150,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
"w": 8,
"x": 16,
"y": 14
},
"id": 10,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"options": {},
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "time",
"targets": [
{
"alias": "",
"expr": "",
"format": "time_series",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "random_walk",
"stringInput": ""
}
],
"thresholds": "81,90",
"title": "last_time display (a few seconds ago)",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [],
"valueName": "last_time"
} }
], ],
"refresh": false, "refresh": false,
......
...@@ -192,7 +192,7 @@ export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefine ...@@ -192,7 +192,7 @@ export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefine
/** /**
* Convert the angular single stat mapping to new react style * Convert the angular single stat mapping to new react style
*/ */
function convertOldAngulrValueMapping(panel: any): ValueMapping[] { export function convertOldAngulrValueMapping(panel: any): ValueMapping[] {
const mappings: ValueMapping[] = []; const mappings: ValueMapping[] = [];
// Guess the right type based on options // Guess the right type based on options
......
...@@ -5,4 +5,5 @@ export { ...@@ -5,4 +5,5 @@ export {
SingleStatBaseOptions, SingleStatBaseOptions,
sharedSingleStatPanelChangedHandler, sharedSingleStatPanelChangedHandler,
sharedSingleStatMigrationHandler, sharedSingleStatMigrationHandler,
convertOldAngulrValueMapping,
} from './SingleStatBaseOptions'; } from './SingleStatBaseOptions';
...@@ -58,6 +58,14 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP ...@@ -58,6 +58,14 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
if (shouldFormat && !_.isBoolean(value)) { if (shouldFormat && !_.isBoolean(value)) {
const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals); const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals);
text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc); text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
// Check if the formatted text mapped to a different value
if (mappings && mappings.length > 0) {
const mappedValue = getMappedValue(mappings, text);
if (mappedValue) {
text = mappedValue.text;
}
}
} }
if (thresholds && thresholds.length) { if (thresholds && thresholds.length) {
color = getColorFromThreshold(numeric, thresholds, theme); color = getColorFromThreshold(numeric, thresholds, theme);
......
...@@ -40,16 +40,18 @@ ...@@ -40,16 +40,18 @@
<h5 class="section-heading">Value</h5> <h5 class="section-heading">Value</h5>
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form" ng-show="ctrl.dataType === 'timeseries'"> <div class="gf-form" ng-if="ctrl.fieldNames.length > 1">
<label class="gf-form-label width-6">Stat</label> <label class="gf-form-label width-6">Field</label>
<div class="gf-form-select-wrapper width-12"> <div class="gf-form-select-wrapper width-12">
<select class="gf-form-input" ng-model="ctrl.panel.valueName" ng-options="f.value as f.text for f in ctrl.valueNameOptions" ng-change="ctrl.refresh()"></select> <select class="gf-form-input" ng-model="ctrl.panel.tableColumn" ng-options="f for f in ctrl.fieldNames" ng-change="ctrl.refresh()"></select>
</div> </div>
</div> </div>
<div class="gf-form" ng-show="ctrl.dataType === 'table'"> </div>
<label class="gf-form-label width-6">Column</label> <div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-6">Show</label>
<div class="gf-form-select-wrapper width-12"> <div class="gf-form-select-wrapper width-12">
<select class="gf-form-input" ng-model="ctrl.panel.tableColumn" ng-options="f for f in ctrl.tableColumnOptions" ng-change="ctrl.refresh()"></select> <select class="gf-form-input" ng-model="ctrl.panel.valueName" ng-options="f.value as f.text for f in ctrl.valueNameOptions" ng-change="ctrl.refresh()"></select>
</div> </div>
</div> </div>
<div class="gf-form"> <div class="gf-form">
...@@ -64,19 +66,24 @@ ...@@ -64,19 +66,24 @@
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-6">Prefix</label> <label class="gf-form-label width-6">Prefix</label>
<input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur> <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-6">Font size</label> <label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select> <select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
</div> </div>
</div> </div>
</div> </div>
<div class="gf-form-inline">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-6">Postfix</label> <label class="gf-form-label width-6">Postfix</label>
<input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur> <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
<label class="gf-form-label width-6">Font size</label> </div>
<div class="gf-form-select-wrapper"> <div class="gf-form">
<select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select> <label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper">
<select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
</div>
</div> </div>
</div> </div>
<div class="gf-form"> <div class="gf-form">
...@@ -122,7 +129,7 @@ ...@@ -122,7 +129,7 @@
<div class="section gf-form-group"> <div class="section gf-form-group">
<h5 class="section-heading">Spark lines</h5> <h5 class="section-heading">Spark lines</h5>
<gf-form-switch class="gf-form" label-class="width-9" label="Show" checked="ctrl.panel.sparkline.show" on-change="ctrl.render()"></gf-form-switch> <gf-form-switch class="gf-form" label-class="width-9" label="Show" checked="ctrl.panel.sparkline.show" on-change="ctrl.refresh()"></gf-form-switch>
<div ng-if="ctrl.panel.sparkline.show"> <div ng-if="ctrl.panel.sparkline.show">
<gf-form-switch class="gf-form" label-class="width-9" label="Full height" checked="ctrl.panel.sparkline.full" on-change="ctrl.render()"></gf-form-switch> <gf-form-switch class="gf-form" label-class="width-9" label="Full height" checked="ctrl.panel.sparkline.full" on-change="ctrl.render()"></gf-form-switch>
<div class="gf-form"> <div class="gf-form">
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
</span> </span>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.panel.mappingType" <select class="gf-form-input" ng-model="ctrl.panel.mappingType"
ng-options="f.value as f.name for f in ctrl.panel.mappingTypes" ng-change="ctrl.render()"></select> ng-options="f.value as f.name for f in ctrl.panel.mappingTypes" ng-change="ctrl.refresh()"></select>
</div> </div>
</div> </div>
</div> </div>
...@@ -18,11 +18,11 @@ ...@@ -18,11 +18,11 @@
<span class="gf-form-label"> <span class="gf-form-label">
<i class="fa fa-remove pointer" ng-click="ctrl.removeValueMap(map)"></i> <i class="fa fa-remove pointer" ng-click="ctrl.removeValueMap(map)"></i>
</span> </span>
<input type="text" ng-model="map.value" placeholder="value" class="gf-form-input max-width-6" ng-blur="ctrl.render()"> <input type="text" ng-model="map.value" placeholder="value" class="gf-form-input max-width-6" ng-blur="ctrl.refresh()">
<span class="gf-form-label"> <span class="gf-form-label">
<i class="fa fa-arrow-right"></i> <i class="fa fa-arrow-right"></i>
</span> </span>
<input type="text" placeholder="text" ng-model="map.text" class="gf-form-input max-width-8" ng-blur="ctrl.render()"> <input type="text" placeholder="text" ng-model="map.text" class="gf-form-input max-width-8" ng-blur="ctrl.refresh()">
</div> </div>
<div class="gf-form-button-row"> <div class="gf-form-button-row">
...@@ -41,11 +41,11 @@ ...@@ -41,11 +41,11 @@
<i class="fa fa-remove pointer" ng-click="ctrl.removeRangeMap(rangeMap)"></i> <i class="fa fa-remove pointer" ng-click="ctrl.removeRangeMap(rangeMap)"></i>
</span> </span>
<span class="gf-form-label">From</span> <span class="gf-form-label">From</span>
<input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="ctrl.render()"> <input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="ctrl.refresh()">
<span class="gf-form-label">To</span> <span class="gf-form-label">To</span>
<input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="ctrl.render()"> <input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="ctrl.refresh()">
<span class="gf-form-label">Text</span> <span class="gf-form-label">Text</span>
<input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="ctrl.render()"> <input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="ctrl.refresh()">
</div> </div>
<div class="gf-form-button-row"> <div class="gf-form-button-row">
......
...@@ -3,34 +3,56 @@ import $ from 'jquery'; ...@@ -3,34 +3,56 @@ import $ from 'jquery';
import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot';
import 'vendor/flot/jquery.flot.gauge'; import 'vendor/flot/jquery.flot.gauge';
import 'app/features/panel/panellinks/link_srv'; import 'app/features/panel/panellinks/link_srv';
import { getDecimalsForValue } from '@grafana/ui'; import {
LegacyResponseData,
getFlotPairs,
getDisplayProcessor,
convertOldAngulrValueMapping,
getColorFromHexRgbOrName,
} from '@grafana/ui';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import config from 'app/core/config'; import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { MetricsPanelCtrl } from 'app/plugins/sdk'; import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { isTableData } from '@grafana/data'; import {
import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui'; DataFrame,
FieldType,
reduceField,
ReducerID,
Field,
GraphSeriesValue,
DisplayValue,
fieldReducers,
KeyValue,
} from '@grafana/data';
import { auto } from 'angular'; import { auto } from 'angular';
import { LinkSrv, LinkModel } from 'app/features/panel/panellinks/link_srv'; import { LinkSrv, LinkModel } from 'app/features/panel/panellinks/link_srv';
import TableModel from 'app/core/table_model'; import { PanelQueryRunnerFormat } from 'app/features/dashboard/state/PanelQueryRunner';
import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
const BASE_FONT_SIZE = 38; const BASE_FONT_SIZE = 38;
interface DataFormat { export interface ShowData {
value: string | number; field: Field;
valueFormatted: string; value: any;
valueRounded: number; sparkline: GraphSeriesValue[][];
display: DisplayValue;
scopedVars: any;
thresholds: any[];
colorMap: any;
} }
class SingleStatCtrl extends MetricsPanelCtrl { class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html'; static templateUrl = 'module.html';
dataType = 'timeseries'; data: Partial<ShowData> = {};
series: any[];
data: any;
fontSizes: any[]; fontSizes: any[];
unitFormats: any[]; unitFormats: any[];
fieldNames: string[] = [];
invalidGaugeRange: boolean; invalidGaugeRange: boolean;
panel: any; panel: any;
events: any; events: any;
...@@ -47,7 +69,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -47,7 +69,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
{ value: 'range', text: 'Range' }, { value: 'range', text: 'Range' },
{ value: 'last_time', text: 'Time of last point' }, { value: 'last_time', text: 'Time of last point' },
]; ];
tableColumnOptions: any;
// Set and populate defaults // Set and populate defaults
panelDefaults: any = { panelDefaults: any = {
...@@ -102,6 +123,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -102,6 +123,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
this.dataFormat = PanelQueryRunnerFormat.frames;
this.onSparklineColorChange = this.onSparklineColorChange.bind(this); this.onSparklineColorChange = this.onSparklineColorChange.bind(this);
this.onSparklineFillChange = this.onSparklineFillChange.bind(this); this.onSparklineFillChange = this.onSparklineFillChange.bind(this);
} }
...@@ -128,104 +151,115 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -128,104 +151,115 @@ class SingleStatCtrl extends MetricsPanelCtrl {
} }
onDataError(err: any) { onDataError(err: any) {
this.onDataReceived([]); this.handleDataFrames([]);
} }
onDataReceived(dataList: any[]) { // This should only be called from the snapshot callback
const data: any = { onDataReceived(dataList: LegacyResponseData[]) {
scopedVars: _.extend({}, this.panel.scopedVars), this.handleDataFrames(getProcessedDataFrames(dataList));
}; }
// Directly support DataFrame skipping event callbacks
handleDataFrames(frames: DataFrame[]) {
const { panel } = this;
super.handleDataFrames(frames);
this.loading = false;
const distinct = getDistinctNames(frames);
let fieldInfo = distinct.byName[panel.tableColumn]; //
this.fieldNames = distinct.names;
if (!fieldInfo) {
fieldInfo = distinct.first;
}
if (dataList.length > 0 && isTableData(dataList[0])) { if (!fieldInfo) {
this.dataType = 'table'; // When we don't have any field
const tableData = dataList.map(this.tableHandler.bind(this)); this.data = {
this.setTableValues(tableData, data); value: 'No Data',
display: {
text: 'No Data',
numeric: NaN,
},
};
} else { } else {
this.dataType = 'timeseries'; this.data = this.processField(fieldInfo);
this.series = dataList.map(this.seriesHandler.bind(this));
this.setValues(data);
} }
this.data = data;
this.render(); this.render();
} }
seriesHandler(dataFrame: any) { processField(fieldInfo: FieldInfo) {
const series = new TimeSeries({ const { panel, dashboard } = this;
datapoints: dataFrame.datapoints || [],
alias: dataFrame.target,
});
series.flotpairs = series.getFlotPairs(this.panel.nullPointMode); const name = fieldInfo.field.config.title || fieldInfo.field.name;
return series; let calc = panel.valueName;
} let calcField = fieldInfo.field;
let val: any = undefined;
tableHandler(tableData: TableModel) { if ('name' === calc) {
const datapoints: any[] = []; val = name;
const columnNames: string[] = []; } else {
if ('last_time' === calc) {
if (fieldInfo.frame.firstTimeField) {
calcField = fieldInfo.frame.firstTimeField;
calc = ReducerID.last;
}
}
tableData.columns.forEach((column, columnIndex) => { // Normalize functions (avg -> mean, etc)
columnNames[columnIndex] = column.text; const r = fieldReducers.getIfExists(calc);
}); if (r) {
calc = r.id;
// With strings, don't accidentally use a math function
if (calcField.type === FieldType.string) {
const avoid = [ReducerID.mean, ReducerID.sum];
if (avoid.includes(calc)) {
calc = panel.valueName = ReducerID.first;
}
}
} else {
calc = ReducerID.lastNotNull;
}
this.tableColumnOptions = columnNames; // Calculate the value
if (!_.find(tableData.columns, ['text', this.panel.tableColumn])) { val = reduceField({
this.setTableColumnToSensibleDefault(tableData); field: calcField,
reducers: [calc],
})[calc];
} }
tableData.rows.forEach(row => { const processor = getDisplayProcessor({
const datapoint: any = {}; field: {
...fieldInfo.field.config,
row.forEach((value: any, columnIndex: number) => { unit: panel.format,
const key = columnNames[columnIndex]; decimals: panel.decimals,
datapoint[key] = value; mappings: convertOldAngulrValueMapping(panel),
}); },
theme: config.theme,
datapoints.push(datapoint); isUtc: dashboard.isTimezoneUtc && dashboard.isTimezoneUtc(),
}); });
return datapoints; const data = {
} field: fieldInfo.field,
value: val,
setTableColumnToSensibleDefault(tableData: TableModel) { display: processor(val),
if (tableData.columns.length === 1) { scopedVars: _.extend({}, panel.scopedVars),
this.panel.tableColumn = tableData.columns[0].text; };
} else {
this.panel.tableColumn = _.find(tableData.columns, col => {
return col.type !== 'time';
}).text;
}
}
setTableValues(tableData: any[], data: DataFormat) {
if (!tableData || tableData.length === 0) {
return;
}
if (tableData[0].length === 0 || tableData[0][0][this.panel.tableColumn] === undefined) {
return;
}
const datapoint = tableData[0][0]; data.scopedVars['__name'] = name;
data.value = datapoint[this.panel.tableColumn]; panel.tableColumn = this.fieldNames.length > 1 ? name : '';
if (_.isString(data.value)) { // Get the fields for a sparkline
data.valueFormatted = _.escape(data.value); if (panel.sparkline && panel.sparkline.show && fieldInfo.frame.firstTimeField) {
data.value = 0; this.data.sparkline = getFlotPairs({
data.valueRounded = 0; xField: fieldInfo.frame.firstTimeField,
} else { yField: fieldInfo.field,
const decimalInfo = getDecimalsForValue(data.value, this.panel.decimals); nullValueMode: panel.nullPointMode,
const formatFunc = getValueFormat(this.panel.format); });
data.valueFormatted = formatFunc(
datapoint[this.panel.tableColumn],
decimalInfo.decimals,
decimalInfo.scaledDecimals
);
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
} }
this.setValueMapping(data); return data;
} }
canModifyText() { canModifyText() {
...@@ -267,106 +301,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -267,106 +301,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
this.render(); this.render();
} }
setValues(data: any) {
data.flotpairs = [];
if (this.series.length > 1) {
const error: any = new Error();
error.message = 'Multiple Series Error';
error.data =
'Metric query returns ' +
this.series.length +
' series. Single Stat Panel expects a single series.\n\nResponse:\n' +
JSON.stringify(this.series);
throw error;
}
if (this.series && this.series.length > 0) {
const lastPoint: any = _.last(this.series[0].datapoints);
const lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
const formatFunc = getValueFormat(this.panel.format);
if (this.panel.valueName === 'name') {
data.value = 0;
data.valueRounded = 0;
data.valueFormatted = this.series[0].alias;
} else if (_.isString(lastValue)) {
data.value = 0;
data.valueFormatted = _.escape(lastValue);
data.valueRounded = 0;
} else if (this.panel.valueName === 'last_time') {
data.value = lastPoint[1];
data.valueRounded = data.value;
data.valueFormatted = formatFunc(data.value, 0, 0, this.dashboard.isTimezoneUtc());
} else {
data.value = this.series[0].stats[this.panel.valueName];
data.flotpairs = this.series[0].flotpairs;
const decimalInfo = getDecimalsForValue(data.value, this.panel.decimals);
data.valueFormatted = formatFunc(
data.value,
decimalInfo.decimals,
decimalInfo.scaledDecimals,
this.dashboard.isTimezoneUtc()
);
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
}
// Add $__name variable for using in prefix or postfix
data.scopedVars['__name'] = { value: this.series[0].label };
}
this.setValueMapping(data);
}
setValueMapping(data: DataFormat) {
// check value to text mappings if its enabled
if (this.panel.mappingType === 1) {
for (let i = 0; i < this.panel.valueMaps.length; i++) {
const map = this.panel.valueMaps[i];
// special null case
if (map.value === 'null') {
if (data.value === null || data.value === void 0) {
data.valueFormatted = map.text;
return;
}
continue;
}
// value/number to text mapping
const value = parseFloat(map.value);
if (value === data.valueRounded) {
data.valueFormatted = map.text;
return;
}
}
} else if (this.panel.mappingType === 2) {
for (let i = 0; i < this.panel.rangeMaps.length; 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) {
data.valueFormatted = map.text;
return;
}
continue;
}
// value/number to range mapping
const from = parseFloat(map.from);
const to = parseFloat(map.to);
if (to >= data.valueRounded && from <= data.valueRounded) {
data.valueFormatted = map.text;
return;
}
}
}
if (data.value === null || data.value === void 0) {
data.valueFormatted = 'no value';
}
}
removeValueMap(map: any) { removeValueMap(map: any) {
const index = _.indexOf(this.panel.valueMaps, map); const index = _.indexOf(this.panel.valueMaps, map);
this.panel.valueMaps.splice(index, 1); this.panel.valueMaps.splice(index, 1);
...@@ -394,12 +328,12 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -394,12 +328,12 @@ class SingleStatCtrl extends MetricsPanelCtrl {
const $sanitize = this.$sanitize; const $sanitize = this.$sanitize;
const panel = ctrl.panel; const panel = ctrl.panel;
const templateSrv = this.templateSrv; const templateSrv = this.templateSrv;
let data: any;
let linkInfo: LinkModel | null = null; let linkInfo: LinkModel | null = null;
const $panelContainer = elem.find('.panel-container'); const $panelContainer = elem.find('.panel-container');
elem = elem.find('.singlestat-panel'); elem = elem.find('.singlestat-panel');
function applyColoringThresholds(valueString: string) { function applyColoringThresholds(valueString: string) {
const data = ctrl.data;
const color = getColorForValue(data, data.value); const color = getColorForValue(data, data.value);
if (color) { if (color) {
return '<span style="color:' + color + '">' + valueString + '</span>'; return '<span style="color:' + color + '">' + valueString + '</span>';
...@@ -409,20 +343,21 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -409,20 +343,21 @@ class SingleStatCtrl extends MetricsPanelCtrl {
} }
function getSpan(className: string, fontSizePercent: string, applyColoring: any, value: string) { function getSpan(className: string, fontSizePercent: string, applyColoring: any, value: string) {
value = $sanitize(templateSrv.replace(value, data.scopedVars)); value = $sanitize(templateSrv.replace(value, ctrl.data.scopedVars));
value = applyColoring ? applyColoringThresholds(value) : value; value = applyColoring ? applyColoringThresholds(value) : value;
const pixelSize = (parseInt(fontSizePercent, 10) / 100) * BASE_FONT_SIZE; const pixelSize = (parseInt(fontSizePercent, 10) / 100) * BASE_FONT_SIZE;
return '<span class="' + className + '" style="font-size:' + pixelSize + 'px">' + value + '</span>'; return '<span class="' + className + '" style="font-size:' + pixelSize + 'px">' + value + '</span>';
} }
function getBigValueHtml() { function getBigValueHtml() {
const data: ShowData = ctrl.data;
let body = '<div class="singlestat-panel-value-container">'; let body = '<div class="singlestat-panel-value-container">';
if (panel.prefix) { if (panel.prefix) {
body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, panel.colorPrefix, panel.prefix); body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, panel.colorPrefix, panel.prefix);
} }
body += getSpan('singlestat-panel-value', panel.valueFontSize, panel.colorValue, data.valueFormatted); body += getSpan('singlestat-panel-value', panel.valueFontSize, panel.colorValue, data.display.text);
if (panel.postfix) { if (panel.postfix) {
body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.colorPostfix, panel.postfix); body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.colorPostfix, panel.postfix);
...@@ -434,14 +369,16 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -434,14 +369,16 @@ class SingleStatCtrl extends MetricsPanelCtrl {
} }
function getValueText() { function getValueText() {
const data: ShowData = ctrl.data;
let result = panel.prefix ? templateSrv.replace(panel.prefix, data.scopedVars) : ''; let result = panel.prefix ? templateSrv.replace(panel.prefix, data.scopedVars) : '';
result += data.valueFormatted; result += data.display.text;
result += panel.postfix ? templateSrv.replace(panel.postfix, data.scopedVars) : ''; result += panel.postfix ? templateSrv.replace(panel.postfix, data.scopedVars) : '';
return result; return result;
} }
function addGauge() { function addGauge() {
const data: ShowData = ctrl.data;
const width = elem.width(); const width = elem.width();
const height = elem.height(); const height = elem.height();
// Allow to use a bit more space for wide gauges // Allow to use a bit more space for wide gauges
...@@ -513,7 +450,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -513,7 +450,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
width: thresholdMarkersWidth, width: thresholdMarkersWidth,
}, },
value: { value: {
color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null, color: panel.colorValue ? getColorForValue(data, data.display.numeric) : null,
formatter: () => { formatter: () => {
return getValueText(); return getValueText();
}, },
...@@ -537,6 +474,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -537,6 +474,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
} }
function addSparkline() { function addSparkline() {
const data: ShowData = ctrl.data;
const width = elem.width(); const width = elem.width();
if (width < 30) { if (width < 30) {
// element has not gotten it's width yet // element has not gotten it's width yet
...@@ -544,6 +482,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -544,6 +482,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
setTimeout(addSparkline, 30); setTimeout(addSparkline, 30);
return; return;
} }
if (!data.sparkline || !data.sparkline.length) {
// no sparkline data
return;
}
const height = ctrl.height; const height = ctrl.height;
const plotCanvas = $('<div></div>'); const plotCanvas = $('<div></div>');
...@@ -592,7 +534,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -592,7 +534,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
elem.append(plotCanvas); elem.append(plotCanvas);
const plotSeries = { const plotSeries = {
data: data.flotpairs, data: data.sparkline,
color: getColorFromHexRgbOrName(panel.sparkline.lineColor, config.theme.type), color: getColorFromHexRgbOrName(panel.sparkline.lineColor, config.theme.type),
}; };
...@@ -603,26 +545,24 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -603,26 +545,24 @@ class SingleStatCtrl extends MetricsPanelCtrl {
if (!ctrl.data) { if (!ctrl.data) {
return; return;
} }
data = ctrl.data; const { data, panel } = ctrl;
// get thresholds // get thresholds
data.thresholds = panel.thresholds.split(',').map((strVale: string) => { data.thresholds = panel.thresholds
return Number(strVale.trim()); ? panel.thresholds.split(',').map((strVale: string) => {
}); return Number(strVale.trim());
})
: [];
// Map panel colors to hex or rgb/a values // Map panel colors to hex or rgb/a values
data.colorMap = panel.colors.map((color: string) => if (panel.colors) {
getColorFromHexRgbOrName( data.colorMap = panel.colors.map((color: string) => getColorFromHexRgbOrName(color, config.theme.type));
color, }
config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark
)
);
const body = panel.gauge.show ? '' : getBigValueHtml(); const body = panel.gauge.show ? '' : getBigValueHtml();
if (panel.colorBackground) { if (panel.colorBackground) {
const color = getColorForValue(data, data.value); const color = getColorForValue(data, data.display.numeric);
console.log(color);
if (color) { if (color) {
$panelContainer.css('background-color', color); $panelContainer.css('background-color', color);
if (scope.fullscreen) { if (scope.fullscreen) {
...@@ -729,4 +669,59 @@ function getColorForValue(data: any, value: number) { ...@@ -729,4 +669,59 @@ function getColorForValue(data: any, value: number) {
return _.first(data.colorMap); return _.first(data.colorMap);
} }
//------------------------------------------------
// Private utility functions
// Somethign like this should be avaliable in a
// DataFrame[] abstraction helper
//------------------------------------------------
interface FrameInfo {
firstTimeField?: Field;
frame: DataFrame;
}
interface FieldInfo {
field: Field;
frame: FrameInfo;
}
interface DistinctFieldsInfo {
first?: FieldInfo;
byName: KeyValue<FieldInfo>;
names: string[];
}
function getDistinctNames(data: DataFrame[]): DistinctFieldsInfo {
const distinct: DistinctFieldsInfo = {
byName: {},
names: [],
};
for (const frame of data) {
const info: FrameInfo = { frame };
for (const field of frame.fields) {
if (field.type === FieldType.time) {
if (!info.firstTimeField) {
info.firstTimeField = field;
}
} else {
const f = { field, frame: info };
if (!distinct.first) {
distinct.first = f;
}
let t = field.config.title;
if (t && !distinct.byName[t]) {
distinct.byName[t] = f;
distinct.names.push(t);
}
t = field.name;
if (t && !distinct.byName[t]) {
distinct.byName[t] = f;
distinct.names.push(t);
}
}
}
}
return distinct;
}
export { SingleStatCtrl, SingleStatCtrl as PanelCtrl, getColorForValue }; export { SingleStatCtrl, SingleStatCtrl as PanelCtrl, getColorForValue };
import { SingleStatCtrl } from '../module'; import { SingleStatCtrl, ShowData } from '../module';
import { dateTime } from '@grafana/data'; import { dateTime, ReducerID } from '@grafana/data';
import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
import { LegacyResponseData } from '@grafana/ui';
interface TestContext {
ctrl: SingleStatCtrl;
input: LegacyResponseData[];
data: Partial<ShowData>;
setup: (setupFunc: any) => void;
}
describe('SingleStatCtrl', () => { describe('SingleStatCtrl', () => {
const ctx = {} as any; const ctx: TestContext = {} as TestContext;
const epoch = 1505826363746; const epoch = 1505826363746;
Date.now = () => epoch; Date.now = () => epoch;
...@@ -37,7 +45,7 @@ describe('SingleStatCtrl', () => { ...@@ -37,7 +45,7 @@ describe('SingleStatCtrl', () => {
// @ts-ignore // @ts-ignore
ctx.ctrl = new SingleStatCtrl($scope, $injector, {} as LinkSrv, $sanitize); ctx.ctrl = new SingleStatCtrl($scope, $injector, {} as LinkSrv, $sanitize);
setupFunc(); setupFunc();
ctx.ctrl.onDataReceived(ctx.data); ctx.ctrl.onDataReceived(ctx.input);
ctx.data = ctx.ctrl.data; ctx.data = ctx.ctrl.data;
}); });
}; };
...@@ -46,40 +54,38 @@ describe('SingleStatCtrl', () => { ...@@ -46,40 +54,38 @@ describe('SingleStatCtrl', () => {
}); });
} }
singleStatScenario('with defaults', (ctx: any) => { singleStatScenario('with defaults', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
}); });
it('Should use series avg as default main value', () => { it('Should use series avg as default main value', () => {
expect(ctx.data.value).toBe(15); expect(ctx.data.value).toBe(15);
expect(ctx.data.valueRounded).toBe(15);
}); });
it('should set formatted falue', () => { it('should set formatted falue', () => {
expect(ctx.data.valueFormatted).toBe('15'); expect(ctx.data.display.text).toBe('15');
}); });
}); });
singleStatScenario('showing serie name instead of value', (ctx: any) => { singleStatScenario('showing serie name instead of value', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }];
ctx.ctrl.panel.valueName = 'name'; ctx.ctrl.panel.valueName = 'name';
}); });
it('Should use series avg as default main value', () => { it('Should use series avg as default main value', () => {
expect(ctx.data.value).toBe(0); expect(ctx.data.value).toBe('test.cpu1');
expect(ctx.data.valueRounded).toBe(0);
}); });
it('should set formatted value', () => { it('should set formatted value', () => {
expect(ctx.data.valueFormatted).toBe('test.cpu1'); expect(ctx.data.display.text).toBe('test.cpu1');
}); });
}); });
singleStatScenario('showing last iso time instead of value', (ctx: any) => { singleStatScenario('showing last iso time instead of value', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.valueName = 'last_time';
ctx.ctrl.panel.format = 'dateTimeAsIso'; ctx.ctrl.panel.format = 'dateTimeAsIso';
ctx.ctrl.dashboard.isTimezoneUtc = () => false; ctx.ctrl.dashboard.isTimezoneUtc = () => false;
...@@ -87,30 +93,29 @@ describe('SingleStatCtrl', () => { ...@@ -87,30 +93,29 @@ describe('SingleStatCtrl', () => {
it('Should use time instead of value', () => { it('Should use time instead of value', () => {
expect(ctx.data.value).toBe(1505634997920); expect(ctx.data.value).toBe(1505634997920);
expect(ctx.data.valueRounded).toBe(1505634997920);
}); });
it('should set formatted value', () => { it('should set formatted value', () => {
expect(dateTime(ctx.data.valueFormatted).valueOf()).toBe(1505634997000); expect(dateTime(ctx.data.display.text).valueOf()).toBe(1505634997000);
}); });
}); });
singleStatScenario('showing last iso time instead of value (in UTC)', (ctx: any) => { singleStatScenario('showing last iso time instead of value (in UTC)', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.valueName = 'last_time';
ctx.ctrl.panel.format = 'dateTimeAsIso'; ctx.ctrl.panel.format = 'dateTimeAsIso';
ctx.ctrl.dashboard.isTimezoneUtc = () => true; ctx.ctrl.dashboard.isTimezoneUtc = () => true;
}); });
it('should set value', () => { it('should set value', () => {
expect(ctx.data.valueFormatted).toBe('1970-01-01 00:00:05'); expect(ctx.data.display.text).toBe('1970-01-01 00:00:05');
}); });
}); });
singleStatScenario('showing last us time instead of value', (ctx: any) => { singleStatScenario('showing last us time instead of value', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.valueName = 'last_time';
ctx.ctrl.panel.format = 'dateTimeAsUS'; ctx.ctrl.panel.format = 'dateTimeAsUS';
ctx.ctrl.dashboard.isTimezoneUtc = () => false; ctx.ctrl.dashboard.isTimezoneUtc = () => false;
...@@ -118,79 +123,76 @@ describe('SingleStatCtrl', () => { ...@@ -118,79 +123,76 @@ describe('SingleStatCtrl', () => {
it('Should use time instead of value', () => { it('Should use time instead of value', () => {
expect(ctx.data.value).toBe(1505634997920); expect(ctx.data.value).toBe(1505634997920);
expect(ctx.data.valueRounded).toBe(1505634997920);
}); });
it('should set formatted value', () => { it('should set formatted value', () => {
expect(ctx.data.valueFormatted).toBe(dateTime(1505634997920).format('MM/DD/YYYY h:mm:ss a')); expect(ctx.data.display.text).toBe(dateTime(1505634997920).format('MM/DD/YYYY h:mm:ss a'));
}); });
}); });
singleStatScenario('showing last us time instead of value (in UTC)', (ctx: any) => { singleStatScenario('showing last us time instead of value (in UTC)', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 5000]] }];
ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.valueName = 'last_time';
ctx.ctrl.panel.format = 'dateTimeAsUS'; ctx.ctrl.panel.format = 'dateTimeAsUS';
ctx.ctrl.dashboard.isTimezoneUtc = () => true; ctx.ctrl.dashboard.isTimezoneUtc = () => true;
}); });
it('should set formatted value', () => { it('should set formatted value', () => {
expect(ctx.data.valueFormatted).toBe('01/01/1970 12:00:05 am'); expect(ctx.data.display.text).toBe('01/01/1970 12:00:05 am');
}); });
}); });
singleStatScenario('showing last time from now instead of value', (ctx: any) => { singleStatScenario('showing last time from now instead of value', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.valueName = 'last_time';
ctx.ctrl.panel.format = 'dateTimeFromNow'; ctx.ctrl.panel.format = 'dateTimeFromNow';
}); });
it('Should use time instead of value', () => { it('Should use time instead of value', () => {
expect(ctx.data.value).toBe(1505634997920); expect(ctx.data.value).toBe(1505634997920);
expect(ctx.data.valueRounded).toBe(1505634997920);
}); });
it('should set formatted value', () => { it('should set formatted value', () => {
expect(ctx.data.valueFormatted).toBe('2 days ago'); expect(ctx.data.display.text).toBe('2 days ago');
}); });
}); });
singleStatScenario('showing last time from now instead of value (in UTC)', (ctx: any) => { singleStatScenario('showing last time from now instead of value (in UTC)', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 12], [20, 1505634997920]] }];
ctx.ctrl.panel.valueName = 'last_time'; ctx.ctrl.panel.valueName = 'last_time';
ctx.ctrl.panel.format = 'dateTimeFromNow'; ctx.ctrl.panel.format = 'dateTimeFromNow';
}); });
it('should set formatted value', () => { it('should set formatted value', () => {
expect(ctx.data.valueFormatted).toBe('2 days ago'); expect(ctx.data.display.text).toBe('2 days ago');
}); });
}); });
singleStatScenario( singleStatScenario(
'MainValue should use same number for decimals as displayed when checking thresholds', 'MainValue should use same number for decimals as displayed when checking thresholds',
(ctx: any) => { (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[99.999, 1], [99.99999, 2]] }];
ctx.ctrl.panel.valueName = 'avg'; ctx.ctrl.panel.valueName = 'avg';
ctx.ctrl.panel.format = 'none'; ctx.ctrl.panel.format = 'none';
}); });
it('Should be rounded', () => { it('Should be rounded', () => {
expect(ctx.data.value).toBe(99.999495); expect(ctx.data.value).toBe(99.999495);
expect(ctx.data.valueRounded).toBe(100);
}); });
it('should set formatted value', () => { it('should set formatted value', () => {
expect(ctx.data.valueFormatted).toBe('100'); expect(ctx.data.display.text).toBe('100');
}); });
} }
); );
singleStatScenario('When value to text mapping is specified', (ctx: any) => { singleStatScenario('When value to text mapping is specified', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[9.9, 1]] }];
ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
}); });
...@@ -198,36 +200,32 @@ describe('SingleStatCtrl', () => { ...@@ -198,36 +200,32 @@ describe('SingleStatCtrl', () => {
expect(ctx.data.value).toBe(9.9); expect(ctx.data.value).toBe(9.9);
}); });
it('round should be rounded up', () => {
expect(ctx.data.valueRounded).toBe(10);
});
it('Should replace value with text', () => { it('Should replace value with text', () => {
expect(ctx.data.valueFormatted).toBe('OK'); expect(ctx.data.display.text).toBe('OK');
}); });
}); });
singleStatScenario('When range to text mapping is specified for first range', (ctx: any) => { singleStatScenario('When range to text mapping is specified for first range', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[41, 50]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[41, 50]] }];
ctx.ctrl.panel.mappingType = 2; ctx.ctrl.panel.mappingType = 2;
ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
}); });
it('Should replace value with text OK', () => { it('Should replace value with text OK', () => {
expect(ctx.data.valueFormatted).toBe('OK'); expect(ctx.data.display.text).toBe('OK');
}); });
}); });
singleStatScenario('When range to text mapping is specified for other ranges', (ctx: any) => { singleStatScenario('When range to text mapping is specified for other ranges', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = [{ target: 'test.cpu1', datapoints: [[65, 75]] }]; ctx.input = [{ target: 'test.cpu1', datapoints: [[65, 75]] }];
ctx.ctrl.panel.mappingType = 2; ctx.ctrl.panel.mappingType = 2;
ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; 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', () => { it('Should replace value with text NOT OK', () => {
expect(ctx.data.valueFormatted).toBe('NOT OK'); expect(ctx.data.display.text).toBe('NOT OK');
}); });
}); });
...@@ -240,9 +238,9 @@ describe('SingleStatCtrl', () => { ...@@ -240,9 +238,9 @@ describe('SingleStatCtrl', () => {
}, },
]; ];
singleStatScenario('with default values', (ctx: any) => { singleStatScenario('with default values', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = tableData; ctx.input = tableData;
ctx.ctrl.panel = { ctx.ctrl.panel = {
emit: () => {}, emit: () => {},
}; };
...@@ -252,17 +250,16 @@ describe('SingleStatCtrl', () => { ...@@ -252,17 +250,16 @@ describe('SingleStatCtrl', () => {
it('Should use first rows value as default main value', () => { it('Should use first rows value as default main value', () => {
expect(ctx.data.value).toBe(15); expect(ctx.data.value).toBe(15);
expect(ctx.data.valueRounded).toBe(15);
}); });
it('should set formatted value', () => { it('should set formatted value', () => {
expect(ctx.data.valueFormatted).toBe('15'); expect(ctx.data.display.text).toBe('15');
}); });
}); });
singleStatScenario('When table data has multiple columns', (ctx: any) => { singleStatScenario('When table data has multiple columns', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = tableData; ctx.input = tableData;
ctx.ctrl.panel.tableColumn = ''; ctx.ctrl.panel.tableColumn = '';
}); });
...@@ -273,29 +270,28 @@ describe('SingleStatCtrl', () => { ...@@ -273,29 +270,28 @@ describe('SingleStatCtrl', () => {
singleStatScenario( singleStatScenario(
'MainValue should use same number for decimals as displayed when checking thresholds', 'MainValue should use same number for decimals as displayed when checking thresholds',
(ctx: any) => { (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = tableData; ctx.input = tableData;
ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2']; ctx.input[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2'];
ctx.ctrl.panel.mappingType = 0; ctx.ctrl.panel.mappingType = 0;
ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.tableColumn = 'mean';
}); });
it('Should be rounded', () => { it('Should be rounded', () => {
expect(ctx.data.value).toBe(99.99999); expect(ctx.data.value).toBe(99.99999);
expect(ctx.data.valueRounded).toBe(100);
}); });
it('should set formatted falue', () => { it('should set formatted falue', () => {
expect(ctx.data.valueFormatted).toBe('100'); expect(ctx.data.display.text).toBe('100');
}); });
} }
); );
singleStatScenario('When value to text mapping is specified', (ctx: any) => { singleStatScenario('When value to text mapping is specified', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = tableData; ctx.input = tableData;
ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2']; ctx.input[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2'];
ctx.ctrl.panel.mappingType = 2; ctx.ctrl.panel.mappingType = 2;
ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.tableColumn = 'mean';
ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }]; ctx.ctrl.panel.valueMaps = [{ value: '10', text: 'OK' }];
...@@ -305,59 +301,60 @@ describe('SingleStatCtrl', () => { ...@@ -305,59 +301,60 @@ describe('SingleStatCtrl', () => {
expect(ctx.data.value).toBe(9.9); expect(ctx.data.value).toBe(9.9);
}); });
it('round should be rounded up', () => { // it('round should be rounded up', () => {
expect(ctx.data.valueRounded).toBe(10); // expect(ctx.data.valueRounded).toBe(10);
}); // });
it('Should replace value with text', () => { it('Should replace value with text', () => {
expect(ctx.data.valueFormatted).toBe('OK'); expect(ctx.data.display.text).toBe('OK');
}); });
}); });
singleStatScenario('When range to text mapping is specified for first range', (ctx: any) => { singleStatScenario('When range to text mapping is specified for first range', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = tableData; ctx.input = tableData;
ctx.data[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2']; ctx.input[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2'];
ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.tableColumn = 'mean';
ctx.ctrl.panel.mappingType = 2; ctx.ctrl.panel.mappingType = 2;
ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }];
}); });
it('Should replace value with text OK', () => { it('Should replace value with text OK', () => {
expect(ctx.data.valueFormatted).toBe('OK'); expect(ctx.data.display.text).toBe('OK');
}); });
}); });
singleStatScenario('When range to text mapping is specified for other ranges', (ctx: any) => { singleStatScenario('When range to text mapping is specified for other ranges', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = tableData; ctx.input = tableData;
ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; ctx.input[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.tableColumn = 'mean';
ctx.ctrl.panel.mappingType = 2; ctx.ctrl.panel.mappingType = 2;
ctx.ctrl.panel.rangeMaps = [{ from: '10', to: '50', text: 'OK' }, { from: '51', to: '100', text: 'NOT OK' }]; 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', () => { it('Should replace value with text NOT OK', () => {
expect(ctx.data.valueFormatted).toBe('NOT OK'); expect(ctx.data.display.text).toBe('NOT OK');
}); });
}); });
singleStatScenario('When value is string', (ctx: any) => { singleStatScenario('When value is string', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = tableData; ctx.input = tableData;
ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; ctx.input[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2'];
ctx.ctrl.panel.tableColumn = 'test1'; ctx.ctrl.panel.tableColumn = 'test1';
ctx.ctrl.panel.valueName = ReducerID.first;
}); });
it('Should replace value with text NOT OK', () => { it('Should replace value with text NOT OK', () => {
expect(ctx.data.valueFormatted).toBe('ignore1'); expect(ctx.data.display.text).toBe('ignore1');
}); });
}); });
singleStatScenario('When value is zero', (ctx: any) => { singleStatScenario('When value is zero', (ctx: TestContext) => {
ctx.setup(() => { ctx.setup(() => {
ctx.data = tableData; ctx.input = tableData;
ctx.data[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2']; ctx.input[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2'];
ctx.ctrl.panel.tableColumn = 'mean'; ctx.ctrl.panel.tableColumn = 'mean';
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment