mirror of https://github.com/grafana/grafana
parent
6a34eb2d9a
commit
8cd54c94e9
@ -0,0 +1,107 @@ |
||||
import { getValueProcessor, getColorFromThreshold } from './valueProcessor'; |
||||
import { getTheme } from '../themes/index'; |
||||
import { GrafanaThemeType } from '../types/theme'; |
||||
import { MappingType, ValueMapping } from '../types/panel'; |
||||
|
||||
describe('Process values', () => { |
||||
const basicConversions = [ |
||||
{ value: null, text: '' }, |
||||
{ value: undefined, text: '' }, |
||||
{ value: 1.23, text: '1.23' }, |
||||
{ value: 1, text: '1' }, |
||||
{ value: 'hello', text: 'hello' }, |
||||
{ value: {}, text: '[object Object]' }, |
||||
{ value: [], text: '' }, |
||||
{ value: [1, 2, 3], text: '1,2,3' }, |
||||
{ value: ['a', 'b', 'c'], text: 'a,b,c' }, |
||||
]; |
||||
|
||||
it('should return return a string for any input value', () => { |
||||
const processor = getValueProcessor(); |
||||
basicConversions.forEach(item => { |
||||
expect(processor(item.value).text).toBe(item.text); |
||||
}); |
||||
}); |
||||
|
||||
it('should add a suffix to any value', () => { |
||||
const processor = getValueProcessor({ |
||||
prefix: 'xxx', |
||||
theme: getTheme(GrafanaThemeType.Dark), |
||||
}); |
||||
basicConversions.forEach(item => { |
||||
expect(processor(item.value).text).toBe('xxx' + item.text); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('Get color from threshold', () => { |
||||
it('should get first threshold color when only one threshold', () => { |
||||
const thresholds = [{ index: 0, value: -Infinity, color: '#7EB26D' }]; |
||||
expect(getColorFromThreshold(49, thresholds)).toEqual('#7EB26D'); |
||||
}); |
||||
|
||||
it('should get the threshold color if value is same as a threshold', () => { |
||||
const thresholds = [ |
||||
{ index: 2, value: 75, color: '#6ED0E0' }, |
||||
{ index: 1, value: 50, color: '#EAB839' }, |
||||
{ index: 0, value: -Infinity, color: '#7EB26D' }, |
||||
]; |
||||
expect(getColorFromThreshold(50, thresholds)).toEqual('#EAB839'); |
||||
}); |
||||
|
||||
it('should get the nearest threshold color between thresholds', () => { |
||||
const thresholds = [ |
||||
{ index: 2, value: 75, color: '#6ED0E0' }, |
||||
{ index: 1, value: 50, color: '#EAB839' }, |
||||
{ index: 0, value: -Infinity, color: '#7EB26D' }, |
||||
]; |
||||
expect(getColorFromThreshold(55, thresholds)).toEqual('#EAB839'); |
||||
}); |
||||
}); |
||||
|
||||
describe('Format value', () => { |
||||
it('should return if value isNaN', () => { |
||||
const valueMappings: ValueMapping[] = []; |
||||
const value = 'N/A'; |
||||
const instance = getValueProcessor({ mappings: valueMappings }); |
||||
|
||||
const result = instance(value); |
||||
|
||||
expect(result.text).toEqual('N/A'); |
||||
}); |
||||
|
||||
it('should return formatted value if there are no value mappings', () => { |
||||
const valueMappings: ValueMapping[] = []; |
||||
const value = '6'; |
||||
|
||||
const instance = getValueProcessor({ mappings: valueMappings, decimals: 1 }); |
||||
|
||||
const result = instance(value); |
||||
|
||||
expect(result.text).toEqual('6.0'); |
||||
}); |
||||
|
||||
it('should return formatted value if there are no matching value mappings', () => { |
||||
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 = getValueProcessor({ mappings: valueMappings, decimals: 1 }); |
||||
|
||||
const result = instance(value); |
||||
|
||||
expect(result.text).toEqual('10.0'); |
||||
}); |
||||
|
||||
it('should return mapped value if there are matching value mappings', () => { |
||||
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 = '11'; |
||||
const instance = getValueProcessor({ mappings: valueMappings, decimals: 1 }); |
||||
|
||||
expect(instance(value).text).toEqual('1-20'); |
||||
}); |
||||
}); |
@ -0,0 +1,97 @@ |
||||
import { ValueMapping, Threshold } from '../types/panel'; |
||||
import _ from 'lodash'; |
||||
import { getValueFormat, DecimalCount } from './valueFormats/valueFormats'; |
||||
import { getMappedValue } from './valueMappings'; |
||||
import { GrafanaTheme, GrafanaThemeType } from '../types/theme'; |
||||
import { getColorFromHexRgbOrName } from './namedColorsPalette'; |
||||
|
||||
export interface DisplayValue { |
||||
text: string; // How the value should be displayed
|
||||
numeric?: number; // the value as a number
|
||||
color?: string; // suggested color
|
||||
} |
||||
|
||||
export interface DisplayValueOptions { |
||||
unit?: string; |
||||
decimals?: DecimalCount; |
||||
scaledDecimals?: DecimalCount; |
||||
isUtc?: boolean; |
||||
|
||||
color?: string; |
||||
mappings?: ValueMapping[]; |
||||
thresholds?: Threshold[]; |
||||
prefix?: string; |
||||
suffix?: string; |
||||
|
||||
noValue?: string; |
||||
theme?: GrafanaTheme; // Will pick 'dark' if not defined
|
||||
} |
||||
|
||||
export type ValueProcessor = (value: any) => DisplayValue; |
||||
|
||||
export function getValueProcessor(options?: DisplayValueOptions): ValueProcessor { |
||||
if (options && !_.isEmpty(options)) { |
||||
const formatFunc = getValueFormat(options.unit || 'none'); |
||||
return (value: any) => { |
||||
const { prefix, suffix, mappings, thresholds, theme } = options; |
||||
let color = options.color; |
||||
|
||||
let text = _.toString(value); |
||||
const numeric = _.toNumber(value); |
||||
|
||||
if (mappings && mappings.length > 0) { |
||||
const mappedValue = getMappedValue(mappings, value); |
||||
if (mappedValue) { |
||||
text = mappedValue.text; |
||||
// TODO? convert the mapped value back to a number?
|
||||
} |
||||
} |
||||
|
||||
if (_.isNumber(numeric)) { |
||||
text = formatFunc(numeric, options.decimals, options.scaledDecimals, options.isUtc); |
||||
if (thresholds && thresholds.length > 0) { |
||||
color = getColorFromThreshold(numeric, thresholds, theme); |
||||
} |
||||
} |
||||
|
||||
if (!text) { |
||||
text = options.noValue ? options.noValue : ''; |
||||
} |
||||
if (prefix) { |
||||
text = prefix + text; |
||||
} |
||||
if (suffix) { |
||||
text = text + suffix; |
||||
} |
||||
return { text, numeric, color }; |
||||
}; |
||||
} |
||||
return toStringProcessor; |
||||
} |
||||
|
||||
function toStringProcessor(value: any): DisplayValue { |
||||
return { text: _.toString(value), numeric: _.toNumber(value) }; |
||||
} |
||||
|
||||
export function getColorFromThreshold(value: number, thresholds: Threshold[], theme?: GrafanaTheme): string { |
||||
const themeType = theme ? theme.type : GrafanaThemeType.Dark; |
||||
|
||||
if (thresholds.length === 1) { |
||||
return getColorFromHexRgbOrName(thresholds[0].color, themeType); |
||||
} |
||||
|
||||
const atThreshold = thresholds.filter(threshold => value === threshold.value)[0]; |
||||
if (atThreshold) { |
||||
return getColorFromHexRgbOrName(atThreshold.color, themeType); |
||||
} |
||||
|
||||
const belowThreshold = thresholds.filter(threshold => value > threshold.value); |
||||
|
||||
if (belowThreshold.length > 0) { |
||||
const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; |
||||
return getColorFromHexRgbOrName(nearestThreshold.color, themeType); |
||||
} |
||||
|
||||
// Use the first threshold as the default color
|
||||
return getColorFromHexRgbOrName(thresholds[0].color, themeType); |
||||
} |
@ -0,0 +1,64 @@ |
||||
// Libraries
|
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
// Components
|
||||
import { FormField, FormLabel, PanelOptionsGroup, UnitPicker } from '@grafana/ui'; |
||||
|
||||
// Types
|
||||
import { DisplayValueOptions } from '@grafana/ui/src/utils/valueProcessor'; |
||||
|
||||
const labelWidth = 6; |
||||
|
||||
export interface Props { |
||||
options: DisplayValueOptions; |
||||
onChange: (options: DisplayValueOptions) => void; |
||||
} |
||||
|
||||
export class DisplayValueEditor extends PureComponent<Props> { |
||||
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); |
||||
|
||||
onDecimalChange = event => { |
||||
if (!isNaN(event.target.value)) { |
||||
this.props.onChange({ |
||||
...this.props.options, |
||||
decimals: parseInt(event.target.value, 10), |
||||
}); |
||||
} else { |
||||
this.props.onChange({ |
||||
...this.props.options, |
||||
decimals: null, |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); |
||||
onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); |
||||
|
||||
render() { |
||||
const { unit, decimals, prefix, suffix } = this.props.options; |
||||
|
||||
let decimalsString = ''; |
||||
if (Number.isFinite(decimals)) { |
||||
decimalsString = decimals.toString(); |
||||
} |
||||
|
||||
return ( |
||||
<PanelOptionsGroup title="Display Value"> |
||||
<div className="gf-form"> |
||||
<FormLabel width={labelWidth}>Unit</FormLabel> |
||||
<UnitPicker defaultValue={unit} onChange={this.onUnitChange} /> |
||||
</div> |
||||
<FormField |
||||
label="Decimals" |
||||
labelWidth={labelWidth} |
||||
placeholder="auto" |
||||
onChange={this.onDecimalChange} |
||||
value={decimalsString} |
||||
type="number" |
||||
/> |
||||
<FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} /> |
||||
<FormField label="Suffix" labelWidth={labelWidth} onChange={this.onSuffixChange} value={suffix || ''} /> |
||||
</PanelOptionsGroup> |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue