mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'grafana/master' into table-reducer
* grafana/master: (75 commits) Explore: Fix log stats for long labels dont test exists in the test... it will fail if not found add random_walk_table scenario add test file add startAt to random walk scenario get values from base options use singlestat base where appropriate feature(explore/table): Add tooltips to explore table (#16007) Update changelog Add check for Env before log Update index.md chore: Cleaning up implicit anys in manage_dashboard.ts and manage_dashboard.test.ts progress: #14714 chore: Cleaning up implicit anys in app.ts progress: #14714 changelog: adds note about closing #15836 changelog: adds note about closing #6359 and #15931 add partial no inheratance improve single stat display revert most options sharing add migration tests ...
This commit is contained in:
@@ -11,16 +11,14 @@ jest.mock('jquery', () => ({
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
||||
unit: 'none',
|
||||
height: 300,
|
||||
width: 300,
|
||||
value: 25,
|
||||
decimals: 0,
|
||||
value: {
|
||||
text: '25',
|
||||
numeric: 25,
|
||||
},
|
||||
theme: getTheme(),
|
||||
orientation: VizOrientation.Horizontal,
|
||||
};
|
||||
|
||||
@@ -3,26 +3,21 @@ import React, { PureComponent, CSSProperties } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// Utils
|
||||
import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
|
||||
import { getColorFromHexRgbOrName, getThresholdForValue, DisplayValue } from '../../utils';
|
||||
|
||||
// Types
|
||||
import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types';
|
||||
import { Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
|
||||
|
||||
const BAR_SIZE_RATIO = 0.8;
|
||||
|
||||
export interface Props extends Themeable {
|
||||
height: number;
|
||||
unit: string;
|
||||
width: number;
|
||||
thresholds: Threshold[];
|
||||
valueMappings: ValueMapping[];
|
||||
value: TimeSeriesValue;
|
||||
value: DisplayValue;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
orientation: VizOrientation;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
decimals?: number;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -32,24 +27,18 @@ export class BarGauge extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
value: 100,
|
||||
unit: 'none',
|
||||
value: {
|
||||
text: '100',
|
||||
numeric: 100,
|
||||
},
|
||||
orientation: VizOrientation.Horizontal,
|
||||
thresholds: [],
|
||||
valueMappings: [],
|
||||
};
|
||||
|
||||
getNumericValue(): number {
|
||||
if (Number.isFinite(this.props.value as number)) {
|
||||
return this.props.value as number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getValueColors(): BarColors {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
|
||||
const activeThreshold = getThresholdForValue(thresholds, value);
|
||||
const activeThreshold = getThresholdForValue(thresholds, value.numeric);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
@@ -78,7 +67,7 @@ export class BarGauge extends PureComponent<Props> {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
|
||||
// if we are past real value the cell is not "on"
|
||||
if (value === null || (positionValue !== null && positionValue > value)) {
|
||||
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
|
||||
return tinycolor(color)
|
||||
.setAlpha(0.15)
|
||||
.toRgbString();
|
||||
@@ -217,18 +206,14 @@ export class BarGauge extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { maxValue, minValue, orientation, unit, decimals } = this.props;
|
||||
const { maxValue, minValue, orientation, value } = this.props;
|
||||
|
||||
const numericValue = this.getNumericValue();
|
||||
const valuePercent = Math.min(numericValue / (maxValue - minValue), 1);
|
||||
|
||||
const formatFunc = getValueFormat(unit);
|
||||
const valueFormatted = formatFunc(numericValue, decimals);
|
||||
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
|
||||
const vertical = orientation === 'vertical';
|
||||
|
||||
return vertical
|
||||
? this.renderVerticalBar(valueFormatted, valuePercent)
|
||||
: this.renderHorizontalLCD(valueFormatted, valuePercent);
|
||||
? this.renderVerticalBar(value.text, valuePercent)
|
||||
: this.renderHorizontalLCD(value.text, valuePercent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Gauge, Props } from './Gauge';
|
||||
import { ValueMapping, MappingType } from '../../types';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
@@ -12,19 +11,16 @@ jest.mock('jquery', () => ({
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
height: 300,
|
||||
width: 300,
|
||||
value: 25,
|
||||
decimals: 0,
|
||||
value: {
|
||||
text: '25',
|
||||
numeric: 25,
|
||||
},
|
||||
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', () => {
|
||||
it('should return first thresholds color for min and max', () => {
|
||||
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 $ from 'jquery';
|
||||
|
||||
import { ValueMapping, Threshold, GrafanaThemeType } from '../../types';
|
||||
import { getMappedValue } from '../../utils/valueMappings';
|
||||
import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
|
||||
import { Threshold, GrafanaThemeType } from '../../types';
|
||||
import { getColorFromHexRgbOrName } from '../../utils';
|
||||
import { Themeable } from '../../index';
|
||||
|
||||
type GaugeValue = string | number | null;
|
||||
import { DisplayValue } from '../../utils/displayValue';
|
||||
|
||||
export interface Props extends Themeable {
|
||||
decimals?: number | null;
|
||||
height: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
thresholds: Threshold[];
|
||||
showThresholdMarkers: boolean;
|
||||
showThresholdLabels: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
unit: string;
|
||||
width: number;
|
||||
value: number;
|
||||
value: DisplayValue;
|
||||
}
|
||||
|
||||
const FONT_SCALE = 1;
|
||||
@@ -32,15 +24,10 @@ export class Gauge extends PureComponent<Props> {
|
||||
|
||||
static defaultProps: Partial<Props> = {
|
||||
maxValue: 100,
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -51,39 +38,6 @@ export class Gauge extends PureComponent<Props> {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
formatValue(value: GaugeValue) {
|
||||
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: GaugeValue): string {
|
||||
const { thresholds, theme } = this.props;
|
||||
|
||||
const activeThreshold = getThresholdForValue(thresholds, value);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
getFormattedThresholds() {
|
||||
const { maxValue, minValue, thresholds, theme } = this.props;
|
||||
|
||||
@@ -112,15 +66,13 @@ export class Gauge extends PureComponent<Props> {
|
||||
draw() {
|
||||
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 backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3;
|
||||
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const fontSize =
|
||||
Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1);
|
||||
const fontSize = Math.min(dimension / 5, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const options: any = {
|
||||
@@ -149,9 +101,9 @@ export class Gauge extends PureComponent<Props> {
|
||||
width: thresholdMarkersWidth,
|
||||
},
|
||||
value: {
|
||||
color: this.getFontColor(value),
|
||||
color: value.color,
|
||||
formatter: () => {
|
||||
return formattedValue;
|
||||
return value.text;
|
||||
},
|
||||
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
||||
},
|
||||
@@ -160,7 +112,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
},
|
||||
};
|
||||
|
||||
const plotSeries = { data: [[0, value]] };
|
||||
const plotSeries = { data: [[0, value.numeric]] };
|
||||
|
||||
try {
|
||||
$.plot(this.canvasElement, [plotSeries], options);
|
||||
|
||||
@@ -120,7 +120,7 @@ $headings-line-height: ${theme.typography.lineHeight.sm} !default;
|
||||
$border-width: ${theme.border.width.sm} !default;
|
||||
|
||||
$border-radius: ${theme.border.radius.md} !default;
|
||||
$border-radius-lg: ${theme.border.radius.lg}!default;
|
||||
$border-radius-lg: ${theme.border.radius.lg} !default;
|
||||
$border-radius-sm: ${theme.border.radius.sm} !default;
|
||||
|
||||
// Page
|
||||
@@ -191,7 +191,6 @@ $btn-padding-y-lg: 11px !default;
|
||||
$btn-padding-x-xl: 21px !default;
|
||||
$btn-padding-y-xl: 11px !default;
|
||||
|
||||
|
||||
$btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
|
||||
|
||||
// sidemenu
|
||||
|
||||
@@ -26,13 +26,21 @@ export interface PanelEditorProps<T = any> {
|
||||
onOptionsChange: (options: T) => void;
|
||||
}
|
||||
|
||||
export type PreservePanelOptionsHandler<TOptions = any> = (pluginId: string, prevOptions: any) => Partial<TOptions>;
|
||||
/**
|
||||
* Called before a panel is initalized
|
||||
*/
|
||||
export type PanelTypeChangedHook<TOptions = any> = (
|
||||
options: Partial<TOptions>,
|
||||
prevPluginId?: string,
|
||||
prevOptions?: any
|
||||
) => Partial<TOptions>;
|
||||
|
||||
export class ReactPanelPlugin<TOptions = any> {
|
||||
panel: ComponentClass<PanelProps<TOptions>>;
|
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||
defaults?: TOptions;
|
||||
preserveOptions?: PreservePanelOptionsHandler<TOptions>;
|
||||
|
||||
panelTypeChangedHook?: PanelTypeChangedHook<TOptions>;
|
||||
|
||||
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
|
||||
this.panel = panel;
|
||||
@@ -46,8 +54,12 @@ export class ReactPanelPlugin<TOptions = any> {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
setPreserveOptionsHandler(handler: PreservePanelOptionsHandler<TOptions>) {
|
||||
this.preserveOptions = handler;
|
||||
/**
|
||||
* Called when the visualization changes.
|
||||
* Lets you keep whatever settings made sense in the previous panel
|
||||
*/
|
||||
setPanelTypeChangedHook(v: PanelTypeChangedHook<TOptions>) {
|
||||
this.panelTypeChangedHook = v;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
157
packages/grafana-ui/src/utils/displayValue.test.ts
Normal file
157
packages/grafana-ui/src/utils/displayValue.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { getDisplayProcessor, getColorFromThreshold, DisplayProcessor, DisplayValue } from './displayValue';
|
||||
import { MappingType, ValueMapping } from '../types/panel';
|
||||
|
||||
function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) {
|
||||
processors.forEach(processor => {
|
||||
const value = processor(input);
|
||||
expect(value.text).toEqual(match.text);
|
||||
if (match.hasOwnProperty('numeric')) {
|
||||
expect(value.numeric).toEqual(match.numeric);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('Process simple display values', () => {
|
||||
// Don't test float values here since the decimal formatting changes
|
||||
const processors = [
|
||||
// Without options, this shortcuts to a much easier implementation
|
||||
getDisplayProcessor(),
|
||||
|
||||
// Add a simple option that is not used (uses a different base class)
|
||||
getDisplayProcessor({ color: '#FFF' }),
|
||||
|
||||
// Add a simple option that is not used (uses a different base class)
|
||||
getDisplayProcessor({ unit: 'locale' }),
|
||||
];
|
||||
|
||||
it('support null', () => {
|
||||
assertSame(null, processors, { text: '', numeric: NaN });
|
||||
});
|
||||
|
||||
it('support undefined', () => {
|
||||
assertSame(undefined, processors, { text: '', numeric: NaN });
|
||||
});
|
||||
|
||||
it('support NaN', () => {
|
||||
assertSame(NaN, processors, { text: 'NaN', numeric: NaN });
|
||||
});
|
||||
|
||||
it('Integer', () => {
|
||||
assertSame(3, processors, { text: '3', numeric: 3 });
|
||||
});
|
||||
|
||||
it('Text to number', () => {
|
||||
assertSame('3', processors, { text: '3', numeric: 3 });
|
||||
});
|
||||
|
||||
it('Simple String', () => {
|
||||
assertSame('hello', processors, { text: 'hello', numeric: NaN });
|
||||
});
|
||||
|
||||
it('empty array', () => {
|
||||
assertSame([], processors, { text: '', numeric: NaN });
|
||||
});
|
||||
|
||||
it('array of text', () => {
|
||||
assertSame(['a', 'b', 'c'], processors, { text: 'a,b,c', numeric: NaN });
|
||||
});
|
||||
|
||||
it('array of numbers', () => {
|
||||
assertSame([1, 2, 3], processors, { text: '1,2,3', numeric: NaN });
|
||||
});
|
||||
|
||||
it('empty object', () => {
|
||||
assertSame({}, processors, { text: '[object Object]', numeric: NaN });
|
||||
});
|
||||
|
||||
it('boolean true', () => {
|
||||
assertSame(true, processors, { text: 'true', numeric: 1 });
|
||||
});
|
||||
|
||||
it('boolean false', () => {
|
||||
assertSame(false, processors, { text: 'false', numeric: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Processor with more configs', () => {
|
||||
it('support prefix & suffix', () => {
|
||||
const processor = getDisplayProcessor({
|
||||
prefix: 'AA_',
|
||||
suffix: '_ZZ',
|
||||
});
|
||||
|
||||
expect(processor('XXX').text).toEqual('AA_XXX_ZZ');
|
||||
});
|
||||
});
|
||||
|
||||
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 = getDisplayProcessor({ 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 = getDisplayProcessor({ 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 = getDisplayProcessor({ 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 = getDisplayProcessor({ mappings: valueMappings, decimals: 1 });
|
||||
|
||||
expect(instance(value).text).toEqual('1-20');
|
||||
});
|
||||
});
|
||||
145
packages/grafana-ui/src/utils/displayValue.ts
Normal file
145
packages/grafana-ui/src/utils/displayValue.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { ValueMapping, Threshold } from '../types';
|
||||
import _ from 'lodash';
|
||||
import { getValueFormat, DecimalCount } from './valueFormats/valueFormats';
|
||||
import { getMappedValue } from './valueMappings';
|
||||
import { GrafanaTheme, GrafanaThemeType } from '../types';
|
||||
import { getColorFromHexRgbOrName } from './namedColorsPalette';
|
||||
import moment from 'moment';
|
||||
|
||||
export interface DisplayValue {
|
||||
text: string; // Show in the UI
|
||||
numeric: number; // Use isNaN to check if it is a real number
|
||||
color?: string; // color based on configs or Threshold
|
||||
}
|
||||
|
||||
export interface DisplayValueOptions {
|
||||
unit?: string;
|
||||
decimals?: DecimalCount;
|
||||
scaledDecimals?: DecimalCount;
|
||||
dateFormat?: string; // If set try to convert numbers to date
|
||||
|
||||
color?: string;
|
||||
mappings?: ValueMapping[];
|
||||
thresholds?: Threshold[];
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
|
||||
// Alternative to empty string
|
||||
noValue?: string;
|
||||
|
||||
// Context
|
||||
isUtc?: boolean;
|
||||
theme?: GrafanaTheme; // Will pick 'dark' if not defined
|
||||
}
|
||||
|
||||
export type DisplayProcessor = (value: any) => DisplayValue;
|
||||
|
||||
export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProcessor {
|
||||
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);
|
||||
let numeric = toNumber(value);
|
||||
|
||||
let shouldFormat = true;
|
||||
if (mappings && mappings.length > 0) {
|
||||
const mappedValue = getMappedValue(mappings, value);
|
||||
if (mappedValue) {
|
||||
text = mappedValue.text;
|
||||
const v = toNumber(text);
|
||||
if (!isNaN(v)) {
|
||||
numeric = v;
|
||||
}
|
||||
shouldFormat = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dateFormat) {
|
||||
const date = toMoment(value, numeric, options.dateFormat);
|
||||
if (date.isValid()) {
|
||||
text = date.format(options.dateFormat);
|
||||
shouldFormat = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNaN(numeric)) {
|
||||
if (shouldFormat && !_.isBoolean(value)) {
|
||||
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 toMoment(value: any, numeric: number, format: string): moment.Moment {
|
||||
if (!isNaN(numeric)) {
|
||||
const v = moment(numeric);
|
||||
if (v.isValid()) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
const v = moment(value, format);
|
||||
if (v.isValid) {
|
||||
return v;
|
||||
}
|
||||
return moment(value); // moment will try to parse the format
|
||||
}
|
||||
|
||||
/** Will return any value as a number or NaN */
|
||||
function toNumber(value: any): number {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
if (value === null || value === undefined || Array.isArray(value)) {
|
||||
return NaN; // lodash calls them 0
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
return _.toNumber(value);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -5,5 +5,6 @@ export * from './colors';
|
||||
export * from './namedColorsPalette';
|
||||
export * from './thresholds';
|
||||
export * from './string';
|
||||
export * from './displayValue';
|
||||
export * from './deprecationWarning';
|
||||
export { getMappedValue } from './valueMappings';
|
||||
|
||||
Reference in New Issue
Block a user