Commit 7a710737 by Marcus Andersson Committed by Torkel Ödegaard

Gauge/BarGauge: Added support for value mapping of "no data"-state to text/value (#20842)

* FieldDisplay: added support for value mapping of no data state.

 Committer: Marcus Andersson <marcus.andersson@grafana.com>

* FieldDisplay: fixed issue when switching between modes and display numeric was null.

* ValueMapping: introduced a private function for checking null values.

* FieldDisplay: refactoring of test setup to reduce duplication.

* Docs: added info about new 'no data' value to text mapping.

* Docs: improved according to feedback. Reverted prettier formatting changes.

* FieldDisplay: removed unused import.
parent e0229045
...@@ -85,6 +85,8 @@ Gauges gives a clear picture of how high a value is in it's context. It's a grea ...@@ -85,6 +85,8 @@ Gauges gives a clear picture of how high a value is in it's context. It's a grea
Value/Range to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message. Value/Range to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message.
If you want to replace the default "No data" text being displayed when no data is available, add a `value to text mapping` from `null` to your preferred custom text value.
<div class="clearfix"></div> <div class="clearfix"></div>
## Troubleshooting ## Troubleshooting
......
import merge from 'lodash/merge';
import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay'; import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
import { toDataFrame } from '../dataframe/processDataFrame'; import { toDataFrame } from '../dataframe/processDataFrame';
import { ReducerID } from '../transformations/fieldReducer'; import { ReducerID } from '../transformations/fieldReducer';
import { Threshold } from '../types/threshold'; import { Threshold } from '../types/threshold';
import { GrafanaTheme } from '../types/theme'; import { GrafanaTheme } from '../types/theme';
import { MappingType } from '../types';
describe('FieldDisplay', () => { describe('FieldDisplay', () => {
it('Construct simple field properties', () => { it('Construct simple field properties', () => {
...@@ -32,33 +34,8 @@ describe('FieldDisplay', () => { ...@@ -32,33 +34,8 @@ describe('FieldDisplay', () => {
expect(field.unit).toEqual('ms'); expect(field.unit).toEqual('ms');
}); });
// Simple test dataset
const options: GetFieldDisplayValuesOptions = {
data: [
toDataFrame({
name: 'Series Name',
fields: [
{ name: 'Field 1', values: ['a', 'b', 'c'] },
{ name: 'Field 2', values: [1, 3, 5] },
{ name: 'Field 3', values: [2, 4, 6] },
],
}),
],
replaceVariables: (value: string) => {
return value; // Return it unchanged
},
fieldOptions: {
calcs: [],
override: {},
defaults: {},
},
theme: {} as GrafanaTheme,
};
it('show first numeric values', () => { it('show first numeric values', () => {
const display = getFieldDisplayValues({ const options = createDisplayOptions({
...options,
fieldOptions: { fieldOptions: {
calcs: [ReducerID.first], calcs: [ReducerID.first],
override: {}, override: {},
...@@ -67,28 +44,24 @@ describe('FieldDisplay', () => { ...@@ -67,28 +44,24 @@ describe('FieldDisplay', () => {
}, },
}, },
}); });
const display = getFieldDisplayValues(options);
expect(display.map(v => v.display.text)).toEqual(['1', '2']); expect(display.map(v => v.display.text)).toEqual(['1', '2']);
// expect(display.map(v => v.display.title)).toEqual([
// 'a * Field 1 * Series Name', // 0
// 'b * Field 2 * Series Name', // 1
// ]);
}); });
it('show last numeric values', () => { it('show last numeric values', () => {
const display = getFieldDisplayValues({ const options = createDisplayOptions({
...options,
fieldOptions: { fieldOptions: {
calcs: [ReducerID.last], calcs: [ReducerID.last],
override: {}, override: {},
defaults: {}, defaults: {},
}, },
}); });
const display = getFieldDisplayValues(options);
expect(display.map(v => v.display.numeric)).toEqual([5, 6]); expect(display.map(v => v.display.numeric)).toEqual([5, 6]);
}); });
it('show all numeric values', () => { it('show all numeric values', () => {
const display = getFieldDisplayValues({ const options = createDisplayOptions({
...options,
fieldOptions: { fieldOptions: {
values: true, // values: true, //
limit: 1000, limit: 1000,
...@@ -97,12 +70,12 @@ describe('FieldDisplay', () => { ...@@ -97,12 +70,12 @@ describe('FieldDisplay', () => {
defaults: {}, defaults: {},
}, },
}); });
const display = getFieldDisplayValues(options);
expect(display.map(v => v.display.numeric)).toEqual([1, 3, 5, 2, 4, 6]); expect(display.map(v => v.display.numeric)).toEqual([1, 3, 5, 2, 4, 6]);
}); });
it('show 2 numeric values (limit)', () => { it('show 2 numeric values (limit)', () => {
const display = getFieldDisplayValues({ const options = createDisplayOptions({
...options,
fieldOptions: { fieldOptions: {
values: true, // values: true, //
limit: 2, limit: 2,
...@@ -111,6 +84,7 @@ describe('FieldDisplay', () => { ...@@ -111,6 +84,7 @@ describe('FieldDisplay', () => {
defaults: {}, defaults: {},
}, },
}); });
const display = getFieldDisplayValues(options);
expect(display.map(v => v.display.numeric)).toEqual([1, 3]); // First 2 are from the first field expect(display.map(v => v.display.numeric)).toEqual([1, 3]); // First 2 are from the first field
}); });
...@@ -132,7 +106,76 @@ describe('FieldDisplay', () => { ...@@ -132,7 +106,76 @@ describe('FieldDisplay', () => {
}); });
it('Should return field thresholds when there is no data', () => { it('Should return field thresholds when there is no data', () => {
const options: GetFieldDisplayValuesOptions = { const options = createEmptyDisplayOptions({
fieldOptions: {
defaults: {
thresholds: [{ color: '#F2495C', value: 50 }],
},
},
});
const display = getFieldDisplayValues(options);
expect(display[0].field.thresholds!.length).toEqual(1);
expect(display[0].display.numeric).toEqual(0);
});
it('Should return field with default text when no mapping or data available', () => {
const options = createEmptyDisplayOptions();
const display = getFieldDisplayValues(options);
expect(display[0].display.text).toEqual('No data');
expect(display[0].display.numeric).toEqual(0);
});
it('Should return field mapped value when there is no data', () => {
const mapEmptyToText = '0';
const options = createEmptyDisplayOptions({
fieldOptions: {
override: {
mappings: [
{
id: 1,
operator: '',
text: mapEmptyToText,
type: MappingType.ValueToText,
value: 'null',
},
],
},
},
});
const display = getFieldDisplayValues(options);
expect(display[0].display.text).toEqual(mapEmptyToText);
expect(display[0].display.numeric).toEqual(0);
});
it('Should always return display numeric 0 when there is no data', () => {
const mapEmptyToText = '0';
const options = createEmptyDisplayOptions({
fieldOptions: {
override: {
mappings: [
{
id: 1,
operator: '',
text: mapEmptyToText,
type: MappingType.ValueToText,
value: 'null',
},
],
},
},
});
const display = getFieldDisplayValues(options);
expect(display[0].display.numeric).toEqual(0);
});
});
function createEmptyDisplayOptions(extend = {}): GetFieldDisplayValuesOptions {
const options = createDisplayOptions(extend);
return Object.assign(options, {
data: [ data: [
{ {
name: 'No data', name: 'No data',
...@@ -140,20 +183,31 @@ describe('FieldDisplay', () => { ...@@ -140,20 +183,31 @@ describe('FieldDisplay', () => {
length: 0, length: 0,
}, },
], ],
});
}
function createDisplayOptions(extend = {}): GetFieldDisplayValuesOptions {
const options: GetFieldDisplayValuesOptions = {
data: [
toDataFrame({
name: 'Series Name',
fields: [
{ name: 'Field 1', values: ['a', 'b', 'c'] },
{ name: 'Field 2', values: [1, 3, 5] },
{ name: 'Field 3', values: [2, 4, 6] },
],
}),
],
replaceVariables: (value: string) => { replaceVariables: (value: string) => {
return value; return value;
}, },
fieldOptions: { fieldOptions: {
calcs: [], calcs: [],
override: {}, override: {},
defaults: { defaults: {},
thresholds: [{ color: '#F2495C', value: 50 }],
},
}, },
theme: {} as GrafanaTheme, theme: {} as GrafanaTheme,
}; };
const display = getFieldDisplayValues(options); return merge<GetFieldDisplayValuesOptions, any>(options, extend);
expect(display[0].field.thresholds!.length).toEqual(1); }
});
});
import toNumber from 'lodash/toNumber'; import toNumber from 'lodash/toNumber';
import toString from 'lodash/toString'; import toString from 'lodash/toString';
import isEmpty from 'lodash/isEmpty';
import { getDisplayProcessor } from './displayProcessor'; import { getDisplayProcessor } from './displayProcessor';
import { getFlotPairs } from '../utils/flotPairs'; import { getFlotPairs } from '../utils/flotPairs';
...@@ -196,16 +197,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi ...@@ -196,16 +197,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
} }
if (values.length === 0) { if (values.length === 0) {
values.push({ values.push(createNoValuesFieldDisplay(options));
name: 'No data',
field: {
...defaults,
},
display: {
numeric: 0,
text: 'No data',
},
});
} else if (values.length === 1 && !fieldOptions.defaults.title) { } else if (values.length === 1 && !fieldOptions.defaults.title) {
// Don't show title for single item // Don't show title for single item
values[0].display.title = undefined; values[0].display.title = undefined;
...@@ -312,3 +304,37 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display ...@@ -312,3 +304,37 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display
} }
return info; return info;
} }
function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): FieldDisplay {
const displayName = 'No data';
const { fieldOptions } = options;
const { defaults, override } = fieldOptions;
const config = getFieldProperties(defaults, {}, override);
const displayProcessor = getDisplayProcessor({
config,
theme: options.theme,
type: FieldType.other,
});
const display = displayProcessor(null);
const text = getDisplayText(display, displayName);
return {
name: displayName,
field: {
...defaults,
},
display: {
text,
numeric: 0,
},
};
}
function getDisplayText(display: DisplayValue, fallback: string): string {
if (!display || isEmpty(display.text)) {
return fallback;
}
return display.text;
}
...@@ -11,7 +11,7 @@ const addValueToTextMappingText = ( ...@@ -11,7 +11,7 @@ const addValueToTextMappingText = (
return allValueMappings; return allValueMappings;
} }
if (value === null && valueToTextMapping.value && valueToTextMapping.value.toLowerCase() === 'null') { if (value === null && isNullValueMap(valueToTextMapping)) {
return allValueMappings.concat(valueToTextMapping); return allValueMappings.concat(valueToTextMapping);
} }
...@@ -84,3 +84,10 @@ const getAllFormattedValueMappings = (valueMappings: ValueMapping[], value: Time ...@@ -84,3 +84,10 @@ const getAllFormattedValueMappings = (valueMappings: ValueMapping[], value: Time
export const getMappedValue = (valueMappings: ValueMapping[], value: TimeSeriesValue): ValueMapping => { export const getMappedValue = (valueMappings: ValueMapping[], value: TimeSeriesValue): ValueMapping => {
return getAllFormattedValueMappings(valueMappings, value)[0]; return getAllFormattedValueMappings(valueMappings, value)[0];
}; };
const isNullValueMap = (mapping: ValueMap): boolean => {
if (!mapping || !mapping.value) {
return false;
}
return mapping.value.toLowerCase() === 'null';
};
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