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.
This commit is contained in:
Marcus Andersson
2019-12-04 14:29:42 +01:00
committed by Torkel Ödegaard
parent e0229045f2
commit 7a710737ae
4 changed files with 151 additions and 62 deletions

View File

@@ -1,8 +1,10 @@
import merge from 'lodash/merge';
import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
import { toDataFrame } from '../dataframe/processDataFrame';
import { ReducerID } from '../transformations/fieldReducer';
import { Threshold } from '../types/threshold';
import { GrafanaTheme } from '../types/theme';
import { MappingType } from '../types';
describe('FieldDisplay', () => {
it('Construct simple field properties', () => {
@@ -32,33 +34,8 @@ describe('FieldDisplay', () => {
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', () => {
const display = getFieldDisplayValues({
...options,
const options = createDisplayOptions({
fieldOptions: {
calcs: [ReducerID.first],
override: {},
@@ -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.title)).toEqual([
// 'a * Field 1 * Series Name', // 0
// 'b * Field 2 * Series Name', // 1
// ]);
});
it('show last numeric values', () => {
const display = getFieldDisplayValues({
...options,
const options = createDisplayOptions({
fieldOptions: {
calcs: [ReducerID.last],
override: {},
defaults: {},
},
});
const display = getFieldDisplayValues(options);
expect(display.map(v => v.display.numeric)).toEqual([5, 6]);
});
it('show all numeric values', () => {
const display = getFieldDisplayValues({
...options,
const options = createDisplayOptions({
fieldOptions: {
values: true, //
limit: 1000,
@@ -97,12 +70,12 @@ describe('FieldDisplay', () => {
defaults: {},
},
});
const display = getFieldDisplayValues(options);
expect(display.map(v => v.display.numeric)).toEqual([1, 3, 5, 2, 4, 6]);
});
it('show 2 numeric values (limit)', () => {
const display = getFieldDisplayValues({
...options,
const options = createDisplayOptions({
fieldOptions: {
values: true, //
limit: 2,
@@ -111,6 +84,7 @@ describe('FieldDisplay', () => {
defaults: {},
},
});
const display = getFieldDisplayValues(options);
expect(display.map(v => v.display.numeric)).toEqual([1, 3]); // First 2 are from the first field
});
@@ -132,28 +106,108 @@ describe('FieldDisplay', () => {
});
it('Should return field thresholds when there is no data', () => {
const options: GetFieldDisplayValuesOptions = {
data: [
{
name: 'No data',
fields: [],
length: 0,
},
],
replaceVariables: (value: string) => {
return value;
},
const options = createEmptyDisplayOptions({
fieldOptions: {
calcs: [],
override: {},
defaults: {
thresholds: [{ color: '#F2495C', value: 50 }],
},
},
theme: {} as GrafanaTheme,
};
});
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: [
{
name: 'No data',
fields: [],
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) => {
return value;
},
fieldOptions: {
calcs: [],
override: {},
defaults: {},
},
theme: {} as GrafanaTheme,
};
return merge<GetFieldDisplayValuesOptions, any>(options, extend);
}

View File

@@ -1,5 +1,6 @@
import toNumber from 'lodash/toNumber';
import toString from 'lodash/toString';
import isEmpty from 'lodash/isEmpty';
import { getDisplayProcessor } from './displayProcessor';
import { getFlotPairs } from '../utils/flotPairs';
@@ -196,16 +197,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
}
if (values.length === 0) {
values.push({
name: 'No data',
field: {
...defaults,
},
display: {
numeric: 0,
text: 'No data',
},
});
values.push(createNoValuesFieldDisplay(options));
} else if (values.length === 1 && !fieldOptions.defaults.title) {
// Don't show title for single item
values[0].display.title = undefined;
@@ -312,3 +304,37 @@ export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): Display
}
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;
}

View File

@@ -11,7 +11,7 @@ const addValueToTextMappingText = (
return allValueMappings;
}
if (value === null && valueToTextMapping.value && valueToTextMapping.value.toLowerCase() === 'null') {
if (value === null && isNullValueMap(valueToTextMapping)) {
return allValueMappings.concat(valueToTextMapping);
}
@@ -84,3 +84,10 @@ const getAllFormattedValueMappings = (valueMappings: ValueMapping[], value: Time
export const getMappedValue = (valueMappings: ValueMapping[], value: TimeSeriesValue): ValueMapping => {
return getAllFormattedValueMappings(valueMappings, value)[0];
};
const isNullValueMap = (mapping: ValueMap): boolean => {
if (!mapping || !mapping.value) {
return false;
}
return mapping.value.toLowerCase() === 'null';
};