mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
make value processing reusable
This commit is contained in:
parent
6a34eb2d9a
commit
8cd54c94e9
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import { Gauge, Props } from './Gauge';
|
import { Gauge, Props } from './Gauge';
|
||||||
import { ValueMapping, MappingType } from '../../types';
|
|
||||||
import { getTheme } from '../../themes';
|
import { getTheme } from '../../themes';
|
||||||
|
|
||||||
jest.mock('jquery', () => ({
|
jest.mock('jquery', () => ({
|
||||||
@ -12,19 +11,16 @@ jest.mock('jquery', () => ({
|
|||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
valueMappings: [],
|
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
prefix: '',
|
|
||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
showThresholdLabels: false,
|
showThresholdLabels: false,
|
||||||
suffix: '',
|
|
||||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
||||||
unit: 'none',
|
|
||||||
stat: 'avg',
|
|
||||||
height: 300,
|
height: 300,
|
||||||
width: 300,
|
width: 300,
|
||||||
value: 25,
|
value: {
|
||||||
decimals: 0,
|
text: '25',
|
||||||
|
numeric: 25,
|
||||||
|
},
|
||||||
theme: getTheme(),
|
theme: getTheme(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,38 +35,6 @@ const setup = (propOverrides?: object) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Get font color', () => {
|
|
||||||
it('should get first threshold color when only one threshold', () => {
|
|
||||||
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
|
||||||
|
|
||||||
expect(instance.getFontColor(49)).toEqual('#7EB26D');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get the threshold color if value is same as a threshold', () => {
|
|
||||||
const { instance } = setup({
|
|
||||||
thresholds: [
|
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
|
||||||
{ index: 1, value: 50, color: '#EAB839' },
|
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(instance.getFontColor(50)).toEqual('#EAB839');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get the nearest threshold color between thresholds', () => {
|
|
||||||
const { instance } = setup({
|
|
||||||
thresholds: [
|
|
||||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
|
||||||
{ index: 1, value: 50, color: '#EAB839' },
|
|
||||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(instance.getFontColor(55)).toEqual('#EAB839');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Get thresholds formatted', () => {
|
describe('Get thresholds formatted', () => {
|
||||||
it('should return first thresholds color for min and max', () => {
|
it('should return first thresholds color for min and max', () => {
|
||||||
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
||||||
@ -98,51 +62,3 @@ describe('Get thresholds formatted', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Format value', () => {
|
|
||||||
it('should return if value isNaN', () => {
|
|
||||||
const valueMappings: ValueMapping[] = [];
|
|
||||||
const value = 'N/A';
|
|
||||||
const { instance } = setup({ valueMappings });
|
|
||||||
|
|
||||||
const result = instance.formatValue(value);
|
|
||||||
|
|
||||||
expect(result).toEqual('N/A');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return formatted value if there are no value mappings', () => {
|
|
||||||
const valueMappings: ValueMapping[] = [];
|
|
||||||
const value = '6';
|
|
||||||
const { instance } = setup({ valueMappings, decimals: 1 });
|
|
||||||
|
|
||||||
const result = instance.formatValue(value);
|
|
||||||
|
|
||||||
expect(result).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 } = setup({ valueMappings, decimals: 1 });
|
|
||||||
|
|
||||||
const result = instance.formatValue(value);
|
|
||||||
|
|
||||||
expect(result).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 } = setup({ valueMappings, decimals: 1 });
|
|
||||||
|
|
||||||
const result = instance.formatValue(value);
|
|
||||||
|
|
||||||
expect(result).toEqual('1-20');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -1,28 +1,20 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { getMappedValue } from '../../utils/valueMappings';
|
|
||||||
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||||
import { Themeable, GrafanaThemeType } from '../../types/theme';
|
import { Themeable, GrafanaThemeType } from '../../types/theme';
|
||||||
import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
|
import { Threshold, BasicGaugeColor } from '../../types/panel';
|
||||||
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
|
import { DisplayValue } from '../../utils/valueProcessor';
|
||||||
|
|
||||||
type TimeSeriesValue = string | number | null;
|
|
||||||
|
|
||||||
export interface Props extends Themeable {
|
export interface Props extends Themeable {
|
||||||
decimals?: number | null;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
valueMappings: ValueMapping[];
|
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
minValue: number;
|
minValue: number;
|
||||||
prefix: string;
|
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
showThresholdMarkers: boolean;
|
showThresholdMarkers: boolean;
|
||||||
showThresholdLabels: boolean;
|
showThresholdLabels: boolean;
|
||||||
stat: string;
|
|
||||||
suffix: string;
|
value: DisplayValue;
|
||||||
unit: string;
|
|
||||||
width: number;
|
|
||||||
value: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FONT_SCALE = 1;
|
const FONT_SCALE = 1;
|
||||||
@ -32,15 +24,10 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
valueMappings: [],
|
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
prefix: '',
|
|
||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
showThresholdLabels: false,
|
showThresholdLabels: false,
|
||||||
suffix: '',
|
|
||||||
thresholds: [],
|
thresholds: [],
|
||||||
unit: 'none',
|
|
||||||
stat: 'avg',
|
|
||||||
theme: GrafanaThemeType.Dark,
|
theme: GrafanaThemeType.Dark,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,49 +39,6 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
formatValue(value: TimeSeriesValue) {
|
|
||||||
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
|
||||||
|
|
||||||
if (isNaN(value as number)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueMappings.length > 0) {
|
|
||||||
const valueMappedValue = getMappedValue(valueMappings, value);
|
|
||||||
if (valueMappedValue) {
|
|
||||||
return `${prefix && prefix + ' '}${valueMappedValue.text}${suffix && ' ' + suffix}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFunc = getValueFormat(unit);
|
|
||||||
const formattedValue = formatFunc(value as number, decimals);
|
|
||||||
const handleNoValueValue = formattedValue || 'no value';
|
|
||||||
|
|
||||||
return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFontColor(value: TimeSeriesValue) {
|
|
||||||
const { thresholds, theme } = this.props;
|
|
||||||
|
|
||||||
if (thresholds.length === 1) {
|
|
||||||
return getColorFromHexRgbOrName(thresholds[0].color, theme.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
|
||||||
if (atThreshold) {
|
|
||||||
return getColorFromHexRgbOrName(atThreshold.color, theme.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
|
|
||||||
|
|
||||||
if (belowThreshold.length > 0) {
|
|
||||||
const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
|
|
||||||
return getColorFromHexRgbOrName(nearestThreshold.color, theme.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return BasicGaugeColor.Red;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedThresholds() {
|
getFormattedThresholds() {
|
||||||
const { maxValue, minValue, thresholds, theme } = this.props;
|
const { maxValue, minValue, thresholds, theme } = this.props;
|
||||||
|
|
||||||
@ -123,15 +67,13 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
draw() {
|
draw() {
|
||||||
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
|
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
|
||||||
|
|
||||||
const formattedValue = this.formatValue(value) as string;
|
|
||||||
const dimension = Math.min(width, height * 1.3);
|
const dimension = Math.min(width, height * 1.3);
|
||||||
const backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3;
|
const backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3;
|
||||||
|
|
||||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||||
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
||||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||||
const fontSize =
|
const fontSize = Math.min(dimension / 5, 100) * this.getFontScale(value.text.length);
|
||||||
Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1);
|
|
||||||
const thresholdLabelFontSize = fontSize / 2.5;
|
const thresholdLabelFontSize = fontSize / 2.5;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@ -160,9 +102,9 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
width: thresholdMarkersWidth,
|
width: thresholdMarkersWidth,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
color: this.getFontColor(value),
|
color: value.color ? value.color : BasicGaugeColor.Red,
|
||||||
formatter: () => {
|
formatter: () => {
|
||||||
return formattedValue;
|
return value.text;
|
||||||
},
|
},
|
||||||
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
||||||
},
|
},
|
||||||
@ -171,7 +113,8 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const plotSeries = { data: [[0, value]] };
|
const numeric = value.numeric !== null ? value.numeric : 0;
|
||||||
|
const plotSeries = { data: [[0, numeric]] };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$.plot(this.canvasElement, [plotSeries], options);
|
$.plot(this.canvasElement, [plotSeries], options);
|
||||||
|
107
packages/grafana-ui/src/utils/valueProcessor.test.ts
Normal file
107
packages/grafana-ui/src/utils/valueProcessor.test.ts
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
});
|
97
packages/grafana-ui/src/utils/valueProcessor.ts
Normal file
97
packages/grafana-ui/src/utils/valueProcessor.ts
Normal file
@ -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);
|
||||||
|
}
|
64
public/app/plugins/panel/gauge/DisplayValueEditor.tsx
Normal file
64
public/app/plugins/panel/gauge/DisplayValueEditor.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -9,30 +9,50 @@ import { Gauge } from '@grafana/ui';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types';
|
import { PanelProps, NullValueMode, BasicGaugeColor } from '@grafana/ui/src/types';
|
||||||
|
import { DisplayValue, getValueProcessor } from '@grafana/ui/src/utils/valueProcessor';
|
||||||
|
|
||||||
interface Props extends PanelProps<GaugeOptions> {}
|
interface Props extends PanelProps<GaugeOptions> {}
|
||||||
interface State {
|
interface State {
|
||||||
value: TimeSeriesValue;
|
value: DisplayValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GaugePanel extends Component<Props, State> {
|
export class GaugePanel extends Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
if (props.options.valueOptions) {
|
||||||
|
console.warn('TODO!! how do we best migration options?');
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
value: this.findValue(props),
|
value: this.findDisplayValue(props),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
if (this.props.panelData !== prevProps.panelData) {
|
if (this.props.panelData !== prevProps.panelData) {
|
||||||
this.setState({ value: this.findValue(this.props) });
|
this.setState({ value: this.findDisplayValue(this.props) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findDisplayValue(props: Props): DisplayValue {
|
||||||
|
const { replaceVariables, options } = this.props;
|
||||||
|
const { displayOptions } = options;
|
||||||
|
|
||||||
|
const prefix = replaceVariables(displayOptions.prefix);
|
||||||
|
const suffix = replaceVariables(displayOptions.suffix);
|
||||||
|
return getValueProcessor({
|
||||||
|
color: BasicGaugeColor.Red, // The default color
|
||||||
|
...displayOptions,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
// ??? theme:getTheme(GrafanaThemeType.Dark), !! how do I get it here???
|
||||||
|
})(this.findValue(props));
|
||||||
|
}
|
||||||
|
|
||||||
findValue(props: Props): number | null {
|
findValue(props: Props): number | null {
|
||||||
const { panelData, options } = props;
|
const { panelData, options } = props;
|
||||||
const { valueOptions } = options;
|
|
||||||
|
|
||||||
if (panelData.timeSeries) {
|
if (panelData.timeSeries) {
|
||||||
const vmSeries = processTimeSeries({
|
const vmSeries = processTimeSeries({
|
||||||
@ -41,7 +61,7 @@ export class GaugePanel extends Component<Props, State> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (vmSeries[0]) {
|
if (vmSeries[0]) {
|
||||||
return vmSeries[0].stats[valueOptions.stat];
|
return vmSeries[0].stats[options.stat];
|
||||||
}
|
}
|
||||||
} else if (panelData.tableData) {
|
} else if (panelData.tableData) {
|
||||||
return panelData.tableData.rows[0].find(prop => prop > 0);
|
return panelData.tableData.rows[0].find(prop => prop > 0);
|
||||||
@ -50,12 +70,9 @@ export class GaugePanel extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { width, height, replaceVariables, options } = this.props;
|
const { width, height, options } = this.props;
|
||||||
const { valueOptions } = options;
|
|
||||||
const { value } = this.state;
|
const { value } = this.state;
|
||||||
|
|
||||||
const prefix = replaceVariables(valueOptions.prefix);
|
|
||||||
const suffix = replaceVariables(valueOptions.suffix);
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Consumer>
|
<ThemeContext.Consumer>
|
||||||
{theme => (
|
{theme => (
|
||||||
@ -63,12 +80,7 @@ export class GaugePanel extends Component<Props, State> {
|
|||||||
value={value}
|
value={value}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
prefix={prefix}
|
|
||||||
suffix={suffix}
|
|
||||||
unit={valueOptions.unit}
|
|
||||||
decimals={valueOptions.decimals}
|
|
||||||
thresholds={options.thresholds}
|
thresholds={options.thresholds}
|
||||||
valueMappings={options.valueMappings}
|
|
||||||
showThresholdLabels={options.showThresholdLabels}
|
showThresholdLabels={options.showThresholdLabels}
|
||||||
showThresholdMarkers={options.showThresholdMarkers}
|
showThresholdMarkers={options.showThresholdMarkers}
|
||||||
minValue={options.minValue}
|
minValue={options.minValue}
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
|
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
|
||||||
import { GaugeOptionsBox } from './GaugeOptionsBox';
|
import { GaugeOptionsBox } from './GaugeOptionsBox';
|
||||||
import { GaugeOptions, SingleStatValueOptions } from './types';
|
import { GaugeOptions, SingleStatValueOptions } from './types';
|
||||||
|
import { DisplayValueEditor } from './DisplayValueEditor';
|
||||||
|
import { DisplayValueOptions } from '@grafana/ui/src/utils/valueProcessor';
|
||||||
|
|
||||||
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||||
@ -31,13 +33,22 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
|||||||
valueOptions,
|
valueOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDisplayOptionsChanged = (displayOptions: DisplayValueOptions) =>
|
||||||
|
this.props.onOptionsChange({
|
||||||
|
...this.props.options,
|
||||||
|
displayOptions,
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onOptionsChange, options } = this.props;
|
const { onOptionsChange, options } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PanelOptionsGrid>
|
<PanelOptionsGrid>
|
||||||
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
|
{/* This just sets the 'stats', that should be moved to somethign more general */}
|
||||||
|
<SingleStatValueEditor onChange={onOptionsChange} options={options} />
|
||||||
|
|
||||||
|
<DisplayValueEditor onChange={this.onDisplayOptionsChanged} options={options.displayOptions} />
|
||||||
<GaugeOptionsBox onOptionsChange={onOptionsChange} options={options} />
|
<GaugeOptionsBox onOptionsChange={onOptionsChange} options={options} />
|
||||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||||
</PanelOptionsGrid>
|
</PanelOptionsGrid>
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { FormField, FormLabel, PanelOptionsGroup, Select, UnitPicker } from '@grafana/ui';
|
import { FormLabel, PanelOptionsGroup, Select } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { SingleStatValueOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
|
|
||||||
const statOptions = [
|
const statOptions = [
|
||||||
{ value: 'min', label: 'Min' },
|
{ value: 'min', label: 'Min' },
|
||||||
@ -24,41 +24,18 @@ const statOptions = [
|
|||||||
const labelWidth = 6;
|
const labelWidth = 6;
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
options: SingleStatValueOptions;
|
options: GaugeOptions;
|
||||||
onChange: (valueOptions: SingleStatValueOptions) => void;
|
onChange: (options: GaugeOptions) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SingleStatValueEditor extends PureComponent<Props> {
|
export class SingleStatValueEditor extends PureComponent<Props> {
|
||||||
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
|
|
||||||
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
|
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.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() {
|
render() {
|
||||||
const { stat, unit, decimals, prefix, suffix } = this.props.options;
|
const { stat } = this.props.options;
|
||||||
|
|
||||||
let decimalsString = '';
|
|
||||||
if (Number.isFinite(decimals)) {
|
|
||||||
decimalsString = decimals.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelOptionsGroup title="Value">
|
<PanelOptionsGroup title="Show Value">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<FormLabel width={labelWidth}>Stat</FormLabel>
|
<FormLabel width={labelWidth}>Stat</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
@ -68,20 +45,6 @@ export class SingleStatValueEditor extends PureComponent<Props> {
|
|||||||
value={statOptions.find(option => option.value === stat)}
|
value={statOptions.find(option => option.value === stat)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
</PanelOptionsGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
import { Threshold, ValueMapping } from '@grafana/ui';
|
||||||
|
import { DisplayValueOptions } from '@grafana/ui/src/utils/valueProcessor';
|
||||||
|
|
||||||
export interface GaugeOptions {
|
export interface GaugeOptions {
|
||||||
valueMappings: ValueMapping[];
|
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
minValue: number;
|
minValue: number;
|
||||||
showThresholdLabels: boolean;
|
showThresholdLabels: boolean;
|
||||||
showThresholdMarkers: boolean;
|
showThresholdMarkers: boolean;
|
||||||
thresholds: Threshold[];
|
|
||||||
valueOptions: SingleStatValueOptions;
|
stat: string;
|
||||||
|
displayOptions: DisplayValueOptions;
|
||||||
|
|
||||||
|
// TODO: migrate to DisplayValueOptions
|
||||||
|
thresholds?: Threshold[];
|
||||||
|
valueMappings?: ValueMapping[];
|
||||||
|
valueOptions?: SingleStatValueOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deprecated -- migrate to */
|
||||||
export interface SingleStatValueOptions {
|
export interface SingleStatValueOptions {
|
||||||
unit: string;
|
unit: string;
|
||||||
suffix: string;
|
suffix: string;
|
||||||
@ -23,13 +30,14 @@ export const defaults: GaugeOptions = {
|
|||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
showThresholdLabels: false,
|
showThresholdLabels: false,
|
||||||
valueOptions: {
|
|
||||||
|
stat: 'avg',
|
||||||
|
displayOptions: {
|
||||||
prefix: '',
|
prefix: '',
|
||||||
suffix: '',
|
suffix: '',
|
||||||
decimals: null,
|
decimals: null,
|
||||||
stat: 'avg',
|
|
||||||
unit: 'none',
|
unit: 'none',
|
||||||
|
mappings: [],
|
||||||
|
thresholds: [],
|
||||||
},
|
},
|
||||||
valueMappings: [],
|
|
||||||
thresholds: [],
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user