diff --git a/devenv/dev-dashboards/panel-singlestat/singlestat_test.json b/devenv/dev-dashboards/panel-singlestat/singlestat_test.json index 773c36b1303..424d09308ef 100644 --- a/devenv/dev-dashboards/panel-singlestat/singlestat_test.json +++ b/devenv/dev-dashboards/panel-singlestat/singlestat_test.json @@ -501,6 +501,237 @@ } ], "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, diff --git a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts index ae104fccbf1..87dff8fc9cf 100644 --- a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts +++ b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts @@ -192,7 +192,7 @@ export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefine /** * Convert the angular single stat mapping to new react style */ -function convertOldAngulrValueMapping(panel: any): ValueMapping[] { +export function convertOldAngulrValueMapping(panel: any): ValueMapping[] { const mappings: ValueMapping[] = []; // Guess the right type based on options diff --git a/packages/grafana-ui/src/components/SingleStatShared/index.ts b/packages/grafana-ui/src/components/SingleStatShared/index.ts index f6fcffd3fd2..41b23573e22 100644 --- a/packages/grafana-ui/src/components/SingleStatShared/index.ts +++ b/packages/grafana-ui/src/components/SingleStatShared/index.ts @@ -5,4 +5,5 @@ export { SingleStatBaseOptions, sharedSingleStatPanelChangedHandler, sharedSingleStatMigrationHandler, + convertOldAngulrValueMapping, } from './SingleStatBaseOptions'; diff --git a/packages/grafana-ui/src/utils/displayProcessor.ts b/packages/grafana-ui/src/utils/displayProcessor.ts index 78e7298e30f..a9f143835af 100644 --- a/packages/grafana-ui/src/utils/displayProcessor.ts +++ b/packages/grafana-ui/src/utils/displayProcessor.ts @@ -58,6 +58,14 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP if (shouldFormat && !_.isBoolean(value)) { const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals); 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) { color = getColorFromThreshold(numeric, thresholds, theme); diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index b2f30c80575..269bfafff02 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -40,16 +40,18 @@
Value
-
- +
+
- +
-
- +
+
+
+
- +
@@ -64,19 +66,24 @@
+
+
- -
- - - -
- +
+
+ + +
+
+ +
+ +
@@ -122,7 +129,7 @@
Spark lines
- +
diff --git a/public/app/plugins/panel/singlestat/mappings.html b/public/app/plugins/panel/singlestat/mappings.html index 856b70db165..2f65efe78e2 100644 --- a/public/app/plugins/panel/singlestat/mappings.html +++ b/public/app/plugins/panel/singlestat/mappings.html @@ -6,7 +6,7 @@
+ ng-options="f.value as f.name for f in ctrl.panel.mappingTypes" ng-change="ctrl.refresh()">
@@ -18,11 +18,11 @@ - + - +
@@ -41,11 +41,11 @@ From - + To - + Text - +
diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index de63a23332a..fa565cdb889 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -3,34 +3,56 @@ import $ from 'jquery'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.gauge'; 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 config from 'app/core/config'; -import TimeSeries from 'app/core/time_series2'; import { MetricsPanelCtrl } from 'app/plugins/sdk'; -import { isTableData } from '@grafana/data'; -import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui'; +import { + DataFrame, + FieldType, + reduceField, + ReducerID, + Field, + GraphSeriesValue, + DisplayValue, + fieldReducers, + KeyValue, +} from '@grafana/data'; import { auto } from 'angular'; 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; -interface DataFormat { - value: string | number; - valueFormatted: string; - valueRounded: number; +export interface ShowData { + field: Field; + value: any; + sparkline: GraphSeriesValue[][]; + display: DisplayValue; + + scopedVars: any; + + thresholds: any[]; + colorMap: any; } class SingleStatCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; - dataType = 'timeseries'; - series: any[]; - data: any; + data: Partial = {}; + fontSizes: any[]; unitFormats: any[]; + fieldNames: string[] = []; + invalidGaugeRange: boolean; panel: any; events: any; @@ -47,7 +69,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { { value: 'range', text: 'Range' }, { value: 'last_time', text: 'Time of last point' }, ]; - tableColumnOptions: any; // Set and populate defaults panelDefaults: any = { @@ -102,6 +123,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.dataFormat = PanelQueryRunnerFormat.frames; + this.onSparklineColorChange = this.onSparklineColorChange.bind(this); this.onSparklineFillChange = this.onSparklineFillChange.bind(this); } @@ -128,104 +151,115 @@ class SingleStatCtrl extends MetricsPanelCtrl { } onDataError(err: any) { - this.onDataReceived([]); + this.handleDataFrames([]); } - onDataReceived(dataList: any[]) { - const data: any = { - scopedVars: _.extend({}, this.panel.scopedVars), - }; + // This should only be called from the snapshot callback + onDataReceived(dataList: LegacyResponseData[]) { + 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])) { - this.dataType = 'table'; - const tableData = dataList.map(this.tableHandler.bind(this)); - this.setTableValues(tableData, data); + if (!fieldInfo) { + // When we don't have any field + this.data = { + value: 'No Data', + display: { + text: 'No Data', + numeric: NaN, + }, + }; } else { - this.dataType = 'timeseries'; - this.series = dataList.map(this.seriesHandler.bind(this)); - this.setValues(data); + this.data = this.processField(fieldInfo); } - this.data = data; this.render(); } - seriesHandler(dataFrame: any) { - const series = new TimeSeries({ - datapoints: dataFrame.datapoints || [], - alias: dataFrame.target, - }); + processField(fieldInfo: FieldInfo) { + const { panel, dashboard } = this; - series.flotpairs = series.getFlotPairs(this.panel.nullPointMode); - return series; - } + const name = fieldInfo.field.config.title || fieldInfo.field.name; + let calc = panel.valueName; + let calcField = fieldInfo.field; + let val: any = undefined; - tableHandler(tableData: TableModel) { - const datapoints: any[] = []; - const columnNames: string[] = []; + if ('name' === calc) { + val = name; + } else { + if ('last_time' === calc) { + if (fieldInfo.frame.firstTimeField) { + calcField = fieldInfo.frame.firstTimeField; + calc = ReducerID.last; + } + } - tableData.columns.forEach((column, columnIndex) => { - columnNames[columnIndex] = column.text; - }); + // Normalize functions (avg -> mean, etc) + 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; - if (!_.find(tableData.columns, ['text', this.panel.tableColumn])) { - this.setTableColumnToSensibleDefault(tableData); + // Calculate the value + val = reduceField({ + field: calcField, + reducers: [calc], + })[calc]; } - tableData.rows.forEach(row => { - const datapoint: any = {}; - - row.forEach((value: any, columnIndex: number) => { - const key = columnNames[columnIndex]; - datapoint[key] = value; - }); - - datapoints.push(datapoint); + const processor = getDisplayProcessor({ + field: { + ...fieldInfo.field.config, + unit: panel.format, + decimals: panel.decimals, + mappings: convertOldAngulrValueMapping(panel), + }, + theme: config.theme, + isUtc: dashboard.isTimezoneUtc && dashboard.isTimezoneUtc(), }); - return datapoints; - } - - setTableColumnToSensibleDefault(tableData: TableModel) { - if (tableData.columns.length === 1) { - 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 data = { + field: fieldInfo.field, + value: val, + display: processor(val), + scopedVars: _.extend({}, panel.scopedVars), + }; - const datapoint = tableData[0][0]; - data.value = datapoint[this.panel.tableColumn]; + data.scopedVars['__name'] = name; + panel.tableColumn = this.fieldNames.length > 1 ? name : ''; - if (_.isString(data.value)) { - data.valueFormatted = _.escape(data.value); - data.value = 0; - data.valueRounded = 0; - } else { - const decimalInfo = getDecimalsForValue(data.value, this.panel.decimals); - 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); + // Get the fields for a sparkline + if (panel.sparkline && panel.sparkline.show && fieldInfo.frame.firstTimeField) { + this.data.sparkline = getFlotPairs({ + xField: fieldInfo.frame.firstTimeField, + yField: fieldInfo.field, + nullValueMode: panel.nullPointMode, + }); } - this.setValueMapping(data); + return data; } canModifyText() { @@ -267,106 +301,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { 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) { const index = _.indexOf(this.panel.valueMaps, map); this.panel.valueMaps.splice(index, 1); @@ -394,12 +328,12 @@ class SingleStatCtrl extends MetricsPanelCtrl { const $sanitize = this.$sanitize; const panel = ctrl.panel; const templateSrv = this.templateSrv; - let data: any; let linkInfo: LinkModel | null = null; const $panelContainer = elem.find('.panel-container'); elem = elem.find('.singlestat-panel'); function applyColoringThresholds(valueString: string) { + const data = ctrl.data; const color = getColorForValue(data, data.value); if (color) { return '' + valueString + ''; @@ -409,20 +343,21 @@ class SingleStatCtrl extends MetricsPanelCtrl { } 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; const pixelSize = (parseInt(fontSizePercent, 10) / 100) * BASE_FONT_SIZE; return '' + value + ''; } function getBigValueHtml() { + const data: ShowData = ctrl.data; let body = '
'; if (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) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.colorPostfix, panel.postfix); @@ -434,14 +369,16 @@ class SingleStatCtrl extends MetricsPanelCtrl { } function getValueText() { + const data: ShowData = ctrl.data; 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) : ''; return result; } function addGauge() { + const data: ShowData = ctrl.data; const width = elem.width(); const height = elem.height(); // Allow to use a bit more space for wide gauges @@ -513,7 +450,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { width: thresholdMarkersWidth, }, value: { - color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null, + color: panel.colorValue ? getColorForValue(data, data.display.numeric) : null, formatter: () => { return getValueText(); }, @@ -537,6 +474,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { } function addSparkline() { + const data: ShowData = ctrl.data; const width = elem.width(); if (width < 30) { // element has not gotten it's width yet @@ -544,6 +482,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { setTimeout(addSparkline, 30); return; } + if (!data.sparkline || !data.sparkline.length) { + // no sparkline data + return; + } const height = ctrl.height; const plotCanvas = $('
'); @@ -592,7 +534,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { elem.append(plotCanvas); const plotSeries = { - data: data.flotpairs, + data: data.sparkline, color: getColorFromHexRgbOrName(panel.sparkline.lineColor, config.theme.type), }; @@ -603,26 +545,24 @@ class SingleStatCtrl extends MetricsPanelCtrl { if (!ctrl.data) { return; } - data = ctrl.data; + const { data, panel } = ctrl; // get thresholds - data.thresholds = panel.thresholds.split(',').map((strVale: string) => { - return Number(strVale.trim()); - }); + data.thresholds = panel.thresholds + ? panel.thresholds.split(',').map((strVale: string) => { + return Number(strVale.trim()); + }) + : []; // Map panel colors to hex or rgb/a values - data.colorMap = panel.colors.map((color: string) => - getColorFromHexRgbOrName( - color, - config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark - ) - ); + if (panel.colors) { + data.colorMap = panel.colors.map((color: string) => getColorFromHexRgbOrName(color, config.theme.type)); + } const body = panel.gauge.show ? '' : getBigValueHtml(); if (panel.colorBackground) { - const color = getColorForValue(data, data.value); - console.log(color); + const color = getColorForValue(data, data.display.numeric); if (color) { $panelContainer.css('background-color', color); if (scope.fullscreen) { @@ -729,4 +669,59 @@ function getColorForValue(data: any, value: number) { 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; + 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 }; diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts index a604c08504e..17dcdcdb9e6 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts @@ -1,9 +1,17 @@ -import { SingleStatCtrl } from '../module'; -import { dateTime } from '@grafana/data'; +import { SingleStatCtrl, ShowData } from '../module'; +import { dateTime, ReducerID } from '@grafana/data'; import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; +import { LegacyResponseData } from '@grafana/ui'; + +interface TestContext { + ctrl: SingleStatCtrl; + input: LegacyResponseData[]; + data: Partial; + setup: (setupFunc: any) => void; +} describe('SingleStatCtrl', () => { - const ctx = {} as any; + const ctx: TestContext = {} as TestContext; const epoch = 1505826363746; Date.now = () => epoch; @@ -37,7 +45,7 @@ describe('SingleStatCtrl', () => { // @ts-ignore ctx.ctrl = new SingleStatCtrl($scope, $injector, {} as LinkSrv, $sanitize); setupFunc(); - ctx.ctrl.onDataReceived(ctx.data); + ctx.ctrl.onDataReceived(ctx.input); ctx.data = ctx.ctrl.data; }); }; @@ -46,40 +54,38 @@ describe('SingleStatCtrl', () => { }); } - singleStatScenario('with defaults', (ctx: any) => { + singleStatScenario('with defaults', (ctx: TestContext) => { 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', () => { expect(ctx.data.value).toBe(15); - expect(ctx.data.valueRounded).toBe(15); }); 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.data = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; + ctx.input = [{ target: 'test.cpu1', datapoints: [[10, 1], [20, 2]] }]; ctx.ctrl.panel.valueName = 'name'; }); it('Should use series avg as default main value', () => { - expect(ctx.data.value).toBe(0); - expect(ctx.data.valueRounded).toBe(0); + expect(ctx.data.value).toBe('test.cpu1'); }); 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.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.format = 'dateTimeAsIso'; ctx.ctrl.dashboard.isTimezoneUtc = () => false; @@ -87,30 +93,29 @@ describe('SingleStatCtrl', () => { it('Should use time instead of value', () => { expect(ctx.data.value).toBe(1505634997920); - expect(ctx.data.valueRounded).toBe(1505634997920); }); 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.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.format = 'dateTimeAsIso'; ctx.ctrl.dashboard.isTimezoneUtc = () => true; }); 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.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.format = 'dateTimeAsUS'; ctx.ctrl.dashboard.isTimezoneUtc = () => false; @@ -118,79 +123,76 @@ describe('SingleStatCtrl', () => { it('Should use time instead of value', () => { expect(ctx.data.value).toBe(1505634997920); - expect(ctx.data.valueRounded).toBe(1505634997920); }); 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.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.format = 'dateTimeAsUS'; ctx.ctrl.dashboard.isTimezoneUtc = () => true; }); 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.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.format = 'dateTimeFromNow'; }); it('Should use time instead of value', () => { expect(ctx.data.value).toBe(1505634997920); - expect(ctx.data.valueRounded).toBe(1505634997920); }); 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.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.format = 'dateTimeFromNow'; }); it('should set formatted value', () => { - expect(ctx.data.valueFormatted).toBe('2 days ago'); + expect(ctx.data.display.text).toBe('2 days ago'); }); }); singleStatScenario( 'MainValue should use same number for decimals as displayed when checking thresholds', - (ctx: any) => { + (ctx: TestContext) => { 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.format = 'none'; }); it('Should be rounded', () => { expect(ctx.data.value).toBe(99.999495); - expect(ctx.data.valueRounded).toBe(100); }); 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.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' }]; }); @@ -198,36 +200,32 @@ describe('SingleStatCtrl', () => { 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', () => { - 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.data = [{ target: 'test.cpu1', datapoints: [[41, 50]] }]; + ctx.input = [{ 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', () => { - 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.data = [{ target: 'test.cpu1', datapoints: [[65, 75]] }]; + ctx.input = [{ 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', () => { - expect(ctx.data.valueFormatted).toBe('NOT OK'); + expect(ctx.data.display.text).toBe('NOT OK'); }); }); @@ -240,9 +238,9 @@ describe('SingleStatCtrl', () => { }, ]; - singleStatScenario('with default values', (ctx: any) => { + singleStatScenario('with default values', (ctx: TestContext) => { ctx.setup(() => { - ctx.data = tableData; + ctx.input = tableData; ctx.ctrl.panel = { emit: () => {}, }; @@ -252,17 +250,16 @@ describe('SingleStatCtrl', () => { it('Should use first rows value as default main value', () => { expect(ctx.data.value).toBe(15); - expect(ctx.data.valueRounded).toBe(15); }); 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.data = tableData; + ctx.input = tableData; ctx.ctrl.panel.tableColumn = ''; }); @@ -273,29 +270,28 @@ describe('SingleStatCtrl', () => { singleStatScenario( 'MainValue should use same number for decimals as displayed when checking thresholds', - (ctx: any) => { + (ctx: TestContext) => { ctx.setup(() => { - ctx.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2']; + ctx.input = tableData; + ctx.input[0].rows[0] = [1492759673649, 'ignore1', 99.99999, 'ignore2']; ctx.ctrl.panel.mappingType = 0; ctx.ctrl.panel.tableColumn = 'mean'; }); it('Should be rounded', () => { expect(ctx.data.value).toBe(99.99999); - expect(ctx.data.valueRounded).toBe(100); }); 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.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 9.9, 'ignore2']; + ctx.input = tableData; + ctx.input[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' }]; @@ -305,59 +301,60 @@ describe('SingleStatCtrl', () => { expect(ctx.data.value).toBe(9.9); }); - it('round should be rounded up', () => { - expect(ctx.data.valueRounded).toBe(10); - }); + // it('round should be rounded up', () => { + // expect(ctx.data.valueRounded).toBe(10); + // }); 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.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 41, 'ignore2']; + ctx.input = tableData; + ctx.input[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', () => { - 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.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; + ctx.input = tableData; + ctx.input[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', () => { - 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.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; + ctx.input = tableData; + ctx.input[0].rows[0] = [1492759673649, 'ignore1', 65, 'ignore2']; ctx.ctrl.panel.tableColumn = 'test1'; + ctx.ctrl.panel.valueName = ReducerID.first; }); 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.data = tableData; - ctx.data[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2']; + ctx.input = tableData; + ctx.input[0].rows[0] = [1492759673649, 'ignore1', 0, 'ignore2']; ctx.ctrl.panel.tableColumn = 'mean'; });