Merge pull request #14972 from grafana/hugoh/rendering-null-as-valuemapping

Added check for null value in ValueMappings and added tests
This commit is contained in:
Torkel Ödegaard 2019-01-22 11:28:44 +01:00 committed by GitHub
commit bc7842e324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 176 additions and 156 deletions

View File

@ -98,83 +98,6 @@ describe('Get thresholds formatted', () => {
});
});
describe('Format value with value mappings', () => {
it('should return undefined with no valuemappings', () => {
const valueMappings: ValueMapping[] = [];
const value = '10';
const { instance } = setup({ valueMappings });
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
expect(result).toBeUndefined();
});
it('should return undefined with no matching valuemappings', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
];
const value = '10';
const { instance } = setup({ valueMappings });
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
expect(result).toBeUndefined();
});
it('should return first matching mapping with lowest id', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
{ id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' },
];
const value = '10';
const { instance } = setup({ valueMappings });
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
expect(result.text).toEqual('1-20');
});
it('should return rangeToText mapping where value equals to', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' },
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '10';
const { instance } = setup({ valueMappings });
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
expect(result.text).toEqual('1-10');
});
it('should return rangeToText mapping where value equals from', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' },
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '10';
const { instance } = setup({ valueMappings });
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
expect(result.text).toEqual('10-20');
});
it('should return rangeToText mapping where value is between from and to', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '10';
const { instance } = setup({ valueMappings });
const result = instance.getFirstFormattedValueMapping(valueMappings, value);
expect(result.text).toEqual('1-20');
});
});
describe('Format value', () => {
it('should return if value isNaN', () => {
const valueMappings: ValueMapping[] = [];

View File

@ -1,20 +1,10 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import {
ValueMapping,
Threshold,
ThemeName,
MappingType,
BasicGaugeColor,
ThemeNames,
ValueMap,
RangeMap,
} from '../../types/panel';
import { ValueMapping, Threshold, ThemeName, BasicGaugeColor, ThemeNames } from '../../types/panel';
import { TimeSeriesVMs } from '../../types/series';
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
type TimeSeriesValue = string | number | null;
import { TimeSeriesValue, getMappedValue } from '../../utils/valueMappings';
export interface Props {
decimals: number;
@ -59,70 +49,6 @@ export class Gauge extends PureComponent<Props> {
this.draw();
}
addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) {
if (!valueToTextMapping.value) {
return allValueMappings;
}
const valueAsNumber = parseFloat(value as string);
const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) {
return allValueMappings;
}
if (valueAsNumber !== valueToTextMappingAsNumber) {
return allValueMappings;
}
return allValueMappings.concat(valueToTextMapping);
}
addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) {
if (!rangeToTextMapping.from || !rangeToTextMapping.to || !value) {
return allValueMappings;
}
const valueAsNumber = parseFloat(value as string);
const fromAsNumber = parseFloat(rangeToTextMapping.from as string);
const toAsNumber = parseFloat(rangeToTextMapping.to as string);
if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) {
return allValueMappings;
}
if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) {
return allValueMappings.concat(rangeToTextMapping);
}
return allValueMappings;
}
getAllFormattedValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) {
const allFormattedValueMappings = valueMappings.reduce(
(allValueMappings, valueMapping) => {
if (valueMapping.type === MappingType.ValueToText) {
allValueMappings = this.addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value);
} else if (valueMapping.type === MappingType.RangeToText) {
allValueMappings = this.addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value);
}
return allValueMappings;
},
[] as ValueMapping[]
);
allFormattedValueMappings.sort((t1, t2) => {
return t1.id - t2.id;
});
return allFormattedValueMappings;
}
getFirstFormattedValueMapping(valueMappings: ValueMapping[], value: TimeSeriesValue) {
return this.getAllFormattedValueMappings(valueMappings, value)[0];
}
formatValue(value: TimeSeriesValue) {
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
@ -131,7 +57,7 @@ export class Gauge extends PureComponent<Props> {
}
if (valueMappings.length > 0) {
const valueMappedValue = this.getFirstFormattedValueMapping(valueMappings, value);
const valueMappedValue = getMappedValue(valueMappings, value);
if (valueMappedValue) {
return `${prefix} ${valueMappedValue.text} ${suffix}`;
}
@ -139,8 +65,9 @@ export class Gauge extends PureComponent<Props> {
const formatFunc = getValueFormat(unit);
const formattedValue = formatFunc(value as number, decimals);
const handleNoValueValue = formattedValue || 'no value';
return `${prefix} ${formattedValue} ${suffix}`;
return `${prefix} ${handleNoValueValue} ${suffix}`;
}
getFontColor(value: TimeSeriesValue) {
@ -204,7 +131,7 @@ export class Gauge extends PureComponent<Props> {
if (timeSeries[0]) {
value = timeSeries[0].stats[stat];
} else {
value = 'N/A';
value = null;
}
const dimension = Math.min(width, height * 1.3);

View File

@ -0,0 +1,81 @@
import { getMappedValue } from './valueMappings';
import { ValueMapping, MappingType } from '../types/panel';
describe('Format value with value mappings', () => {
it('should return undefined with no valuemappings', () => {
const valueMappings: ValueMapping[] = [];
const value = '10';
expect(getMappedValue(valueMappings, value)).toBeUndefined();
});
it('should return undefined with no matching valuemappings', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
];
const value = '10';
expect(getMappedValue(valueMappings, value)).toBeUndefined();
});
it('should return first matching mapping with lowest id', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
{ id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' },
];
const value = '10';
expect(getMappedValue(valueMappings, value).text).toEqual('1-20');
});
it('should return if value is null and value to text mapping value is null', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
{ id: 1, operator: '', text: '<NULL>', type: MappingType.ValueToText, value: 'null' },
];
const value = null;
expect(getMappedValue(valueMappings, value).text).toEqual('<NULL>');
});
it('should return if value is null and range to text mapping from and to is null', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '<NULL>', type: MappingType.RangeToText, from: 'null', to: 'null' },
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = null;
expect(getMappedValue(valueMappings, value).text).toEqual('<NULL>');
});
it('should return rangeToText mapping where value equals to', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' },
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '10';
expect(getMappedValue(valueMappings, value).text).toEqual('1-10');
});
it('should return rangeToText mapping where value equals from', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' },
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '10';
expect(getMappedValue(valueMappings, value).text).toEqual('10-20');
});
it('should return rangeToText mapping where value is between from and to', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '10';
expect(getMappedValue(valueMappings, value).text).toEqual('1-20');
});
});

View File

@ -0,0 +1,89 @@
import { ValueMapping, MappingType, ValueMap, RangeMap } from '../types';
export type TimeSeriesValue = string | number | null;
const addValueToTextMappingText = (
allValueMappings: ValueMapping[],
valueToTextMapping: ValueMap,
value: TimeSeriesValue
) => {
if (valueToTextMapping.value === undefined) {
return allValueMappings;
}
if (value === null && valueToTextMapping.value && valueToTextMapping.value.toLowerCase() === 'null') {
return allValueMappings.concat(valueToTextMapping);
}
const valueAsNumber = parseFloat(value as string);
const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string);
if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) {
return allValueMappings;
}
if (valueAsNumber !== valueToTextMappingAsNumber) {
return allValueMappings;
}
return allValueMappings.concat(valueToTextMapping);
};
const addRangeToTextMappingText = (
allValueMappings: ValueMapping[],
rangeToTextMapping: RangeMap,
value: TimeSeriesValue
) => {
if (rangeToTextMapping.from === undefined || rangeToTextMapping.to === undefined || value === undefined) {
return allValueMappings;
}
if (
value === null &&
rangeToTextMapping.from &&
rangeToTextMapping.to &&
rangeToTextMapping.from.toLowerCase() === 'null' &&
rangeToTextMapping.to.toLowerCase() === 'null'
) {
return allValueMappings.concat(rangeToTextMapping);
}
const valueAsNumber = parseFloat(value as string);
const fromAsNumber = parseFloat(rangeToTextMapping.from as string);
const toAsNumber = parseFloat(rangeToTextMapping.to as string);
if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) {
return allValueMappings;
}
if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) {
return allValueMappings.concat(rangeToTextMapping);
}
return allValueMappings;
};
const getAllFormattedValueMappings = (valueMappings: ValueMapping[], value: TimeSeriesValue) => {
const allFormattedValueMappings = valueMappings.reduce(
(allValueMappings, valueMapping) => {
if (valueMapping.type === MappingType.ValueToText) {
allValueMappings = addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value);
} else if (valueMapping.type === MappingType.RangeToText) {
allValueMappings = addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value);
}
return allValueMappings;
},
[] as ValueMapping[]
);
allFormattedValueMappings.sort((t1, t2) => {
return t1.id - t2.id;
});
return allFormattedValueMappings;
};
export const getMappedValue = (valueMappings: ValueMapping[], value: TimeSeriesValue): ValueMapping => {
return getAllFormattedValueMappings(valueMappings, value)[0];
};