mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #15925 from ryantxu/reusable-formatting-options
make value processing/formatting more reusable
This commit is contained in:
commit
bfa54d2e26
@ -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);
|
||||
|
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';
|
||||
|
@ -25,6 +25,7 @@ import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
|
||||
import * as tablePanel from 'app/plugins/panel/table/module';
|
||||
import * as table2Panel from 'app/plugins/panel/table2/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as singlestatPanel2 from 'app/plugins/panel/singlestat2/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as gaugePanel from 'app/plugins/panel/gauge/module';
|
||||
import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
|
||||
@ -57,6 +58,7 @@ const builtInPlugins = {
|
||||
'app/plugins/panel/table/module': tablePanel,
|
||||
'app/plugins/panel/table2/module': table2Panel,
|
||||
'app/plugins/panel/singlestat/module': singlestatPanel,
|
||||
'app/plugins/panel/singlestat2/module': singlestatPanel2,
|
||||
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
|
||||
'app/plugins/panel/gauge/module': gaugePanel,
|
||||
'app/plugins/panel/bargauge/module': barGaugePanel,
|
||||
|
@ -2,55 +2,46 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & Utils
|
||||
import { processSingleStatPanelData } from '@grafana/ui';
|
||||
import { DisplayValue, PanelProps, BarGauge } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
// Components
|
||||
import { BarGauge, VizRepeater } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { BarGaugeOptions } from './types';
|
||||
import { PanelProps, SingleStatValueInfo } from '@grafana/ui/src/types';
|
||||
import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
|
||||
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
|
||||
|
||||
interface Props extends PanelProps<BarGaugeOptions> {}
|
||||
|
||||
export class BarGaugePanel extends PureComponent<Props> {
|
||||
renderBarGauge(value: SingleStatValueInfo, width, height) {
|
||||
const { replaceVariables, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
|
||||
const prefix = replaceVariables(valueOptions.prefix);
|
||||
const suffix = replaceVariables(valueOptions.suffix);
|
||||
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
|
||||
const { options } = this.props;
|
||||
|
||||
return (
|
||||
<BarGauge
|
||||
value={value.value as number | null}
|
||||
value={value}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
orientation={options.orientation}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
theme={config.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getProcessedValues = (): DisplayValue[] => {
|
||||
return getSingleStatValues(this.props);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { panelData, options, width, height } = this.props;
|
||||
|
||||
const values = processSingleStatPanelData({
|
||||
panelData: panelData,
|
||||
stat: options.valueOptions.stat,
|
||||
});
|
||||
|
||||
const { height, width, options, panelData } = this.props;
|
||||
const { orientation } = options;
|
||||
return (
|
||||
<VizRepeater height={height} width={width} values={values} orientation={options.orientation}>
|
||||
{({ vizHeight, vizWidth, value }) => this.renderBarGauge(value, vizWidth, vizHeight)}
|
||||
</VizRepeater>
|
||||
<ProcessedValuesRepeater
|
||||
getProcessedValues={this.getProcessedValues}
|
||||
renderValue={this.renderValue}
|
||||
width={width}
|
||||
height={height}
|
||||
source={panelData}
|
||||
orientation={orientation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
|
||||
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
|
||||
import { BarGaugeOptions, orientationOptions } from './types';
|
||||
import { SingleStatValueOptions } from '../gauge/types';
|
||||
import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
|
||||
import { SingleStatValueOptions } from '../singlestat2/types';
|
||||
|
||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
|
@ -3,18 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
|
||||
import { BarGaugePanel } from './BarGaugePanel';
|
||||
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
|
||||
import { BarGaugeOptions, defaults } from './types';
|
||||
import { singleStatBaseOptionsCheck } from '../singlestat2/module';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel);
|
||||
|
||||
reactPanel.setEditor(BarGaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPanelTypeChangedHook((options: BarGaugeOptions, prevPluginId?: string, prevOptions?: any) => {
|
||||
if (prevOptions && prevOptions.valueOptions) {
|
||||
options.valueOptions = prevOptions.valueOptions;
|
||||
options.thresholds = prevOptions.thresholds;
|
||||
options.maxValue = prevOptions.maxValue;
|
||||
options.minValue = prevOptions.minValue;
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
|
||||
|
@ -1,20 +1,17 @@
|
||||
import { Threshold, SelectOptionItem, ValueMapping, VizOrientation } from '@grafana/ui';
|
||||
import { SingleStatValueOptions } from '../gauge/types';
|
||||
import { VizOrientation, SelectOptionItem } from '@grafana/ui';
|
||||
|
||||
export interface BarGaugeOptions {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
orientation: VizOrientation;
|
||||
valueOptions: SingleStatValueOptions;
|
||||
valueMappings: ValueMapping[];
|
||||
thresholds: Threshold[];
|
||||
}
|
||||
import { SingleStatBaseOptions } from '../singlestat2/types';
|
||||
|
||||
export const orientationOptions: SelectOptionItem[] = [
|
||||
{ value: VizOrientation.Horizontal, label: 'Horizontal' },
|
||||
{ value: VizOrientation.Vertical, label: 'Vertical' },
|
||||
];
|
||||
|
||||
export interface BarGaugeOptions extends SingleStatBaseOptions {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
}
|
||||
|
||||
export const defaults: BarGaugeOptions = {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
|
@ -2,37 +2,27 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & Utils
|
||||
import { processSingleStatPanelData } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
// Components
|
||||
import { Gauge, VizRepeater } from '@grafana/ui';
|
||||
import { Gauge } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { GaugeOptions } from './types';
|
||||
import { PanelProps, VizOrientation, SingleStatValueInfo } from '@grafana/ui/src/types';
|
||||
import { DisplayValue, PanelProps } from '@grafana/ui';
|
||||
import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
|
||||
import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
|
||||
|
||||
interface Props extends PanelProps<GaugeOptions> {}
|
||||
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
renderGauge(value: SingleStatValueInfo, width, height) {
|
||||
const { replaceVariables, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
|
||||
const prefix = replaceVariables(valueOptions.prefix);
|
||||
const suffix = replaceVariables(valueOptions.suffix);
|
||||
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
|
||||
const { options } = this.props;
|
||||
|
||||
return (
|
||||
<Gauge
|
||||
value={value.value as number | null}
|
||||
value={value}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={options.minValue}
|
||||
@ -40,20 +30,24 @@ export class GaugePanel extends PureComponent<Props> {
|
||||
theme={config.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getProcessedValues = (): DisplayValue[] => {
|
||||
return getSingleStatValues(this.props);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { panelData, options, height, width } = this.props;
|
||||
|
||||
const values = processSingleStatPanelData({
|
||||
panelData: panelData,
|
||||
stat: options.valueOptions.stat,
|
||||
});
|
||||
|
||||
const { height, width, options, panelData } = this.props;
|
||||
const { orientation } = options;
|
||||
return (
|
||||
<VizRepeater height={height} width={width} values={values} orientation={VizOrientation.Auto}>
|
||||
{({ vizHeight, vizWidth, value }) => this.renderGauge(value, vizWidth, vizHeight)}
|
||||
</VizRepeater>
|
||||
<ProcessedValuesRepeater
|
||||
getProcessedValues={this.getProcessedValues}
|
||||
renderValue={this.renderValue}
|
||||
width={width}
|
||||
height={height}
|
||||
source={panelData}
|
||||
orientation={orientation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,10 @@ import {
|
||||
ValueMapping,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
|
||||
import { GaugeOptionsBox } from './GaugeOptionsBox';
|
||||
import { GaugeOptions, SingleStatValueOptions } from './types';
|
||||
import { GaugeOptions } from './types';
|
||||
import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
|
||||
import { SingleStatValueOptions } from '../singlestat2/types';
|
||||
|
||||
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
|
@ -3,18 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
|
||||
import { GaugePanelEditor } from './GaugePanelEditor';
|
||||
import { GaugePanel } from './GaugePanel';
|
||||
import { GaugeOptions, defaults } from './types';
|
||||
import { singleStatBaseOptionsCheck } from '../singlestat2/module';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
|
||||
|
||||
reactPanel.setEditor(GaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPanelTypeChangedHook((options: GaugeOptions, prevPluginId?: string, prevOptions?: any) => {
|
||||
if (prevOptions && prevOptions.valueOptions) {
|
||||
options.valueOptions = prevOptions.valueOptions;
|
||||
options.thresholds = prevOptions.thresholds;
|
||||
options.maxValue = prevOptions.maxValue;
|
||||
options.minValue = prevOptions.minValue;
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
|
||||
|
@ -1,21 +1,11 @@
|
||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
||||
import { SingleStatBaseOptions } from '../singlestat2/types';
|
||||
import { VizOrientation } from '@grafana/ui';
|
||||
|
||||
export interface GaugeOptions {
|
||||
valueMappings: ValueMapping[];
|
||||
export interface GaugeOptions extends SingleStatBaseOptions {
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
thresholds: Threshold[];
|
||||
valueOptions: SingleStatValueOptions;
|
||||
}
|
||||
|
||||
export interface SingleStatValueOptions {
|
||||
unit: string;
|
||||
suffix: string;
|
||||
stat: string;
|
||||
prefix: string;
|
||||
decimals?: number | null;
|
||||
}
|
||||
|
||||
export const defaults: GaugeOptions = {
|
||||
@ -32,4 +22,5 @@ export const defaults: GaugeOptions = {
|
||||
},
|
||||
valueMappings: [],
|
||||
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
|
||||
orientation: VizOrientation.Auto,
|
||||
};
|
||||
|
@ -0,0 +1,48 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { VizOrientation } from '@grafana/ui';
|
||||
import { VizRepeater } from '@grafana/ui';
|
||||
|
||||
export interface Props<T> {
|
||||
width: number;
|
||||
height: number;
|
||||
orientation: VizOrientation;
|
||||
source: any; // If this changes, the values will be processed
|
||||
processFlag?: boolean; // change to force processing
|
||||
|
||||
getProcessedValues: () => T[];
|
||||
renderValue: (value: T, width: number, height: number) => JSX.Element;
|
||||
}
|
||||
|
||||
interface State<T> {
|
||||
values: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is essentially a cache of processed values. This checks for changes
|
||||
* to the source and then saves the processed values in the State
|
||||
*/
|
||||
export class ProcessedValuesRepeater<T> extends PureComponent<Props<T>, State<T>> {
|
||||
constructor(props: Props<T>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
values: props.getProcessedValues(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props<T>) {
|
||||
const { processFlag, source } = this.props;
|
||||
if (processFlag !== prevProps.processFlag || source !== prevProps.source) {
|
||||
this.setState({ values: this.props.getProcessedValues() });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { orientation, height, width, renderValue } = this.props;
|
||||
const { values } = this.state;
|
||||
return (
|
||||
<VizRepeater height={height} width={width} values={values} orientation={orientation}>
|
||||
{({ vizHeight, vizWidth, value }) => renderValue(value, vizWidth, vizHeight)}
|
||||
</VizRepeater>
|
||||
);
|
||||
}
|
||||
}
|
9
public/app/plugins/panel/singlestat2/README.md
Normal file
9
public/app/plugins/panel/singlestat2/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Singlestat Panel - Native Plugin
|
||||
|
||||
The Singlestat Panel is **included** with Grafana.
|
||||
|
||||
The Singlestat Panel allows you to show the one main summary stat of a SINGLE series. It reduces the series into a single number (by looking at the max, min, average, or sum of values in the series). Singlestat also provides thresholds to color the stat or the Panel background. It can also translate the single number into a text value, and show a sparkline summary of the series.
|
||||
|
||||
Read more about it here:
|
||||
|
||||
[http://docs.grafana.org/reference/singlestat/](http://docs.grafana.org/reference/singlestat/)
|
48
public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
Normal file
48
public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
PanelEditorProps,
|
||||
ThresholdsEditor,
|
||||
Threshold,
|
||||
PanelOptionsGrid,
|
||||
ValueMappingsEditor,
|
||||
ValueMapping,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { SingleStatOptions, SingleStatValueOptions } from './types';
|
||||
import { SingleStatValueEditor } from './SingleStatValueEditor';
|
||||
|
||||
export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
thresholds,
|
||||
});
|
||||
|
||||
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
valueMappings,
|
||||
});
|
||||
|
||||
onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
|
||||
this.props.onOptionsChange({
|
||||
...this.props.options,
|
||||
valueOptions,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { options } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGrid>
|
||||
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
66
public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
Normal file
66
public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
// Libraries
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
|
||||
// Types
|
||||
import { SingleStatOptions, SingleStatBaseOptions } from './types';
|
||||
|
||||
import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { getDisplayProcessor } from '@grafana/ui';
|
||||
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
|
||||
|
||||
export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => {
|
||||
const { panelData, replaceVariables, options } = props;
|
||||
const { valueOptions, valueMappings } = options;
|
||||
const processor = getDisplayProcessor({
|
||||
unit: valueOptions.unit,
|
||||
decimals: valueOptions.decimals,
|
||||
mappings: valueMappings,
|
||||
thresholds: options.thresholds,
|
||||
|
||||
prefix: replaceVariables(valueOptions.prefix),
|
||||
suffix: replaceVariables(valueOptions.suffix),
|
||||
theme: config.theme,
|
||||
});
|
||||
return processSingleStatPanelData({
|
||||
panelData: panelData,
|
||||
stat: valueOptions.stat,
|
||||
}).map(stat => processor(stat.value));
|
||||
};
|
||||
|
||||
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
|
||||
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
|
||||
const style: CSSProperties = {};
|
||||
style.margin = '0 auto';
|
||||
style.fontSize = '250%';
|
||||
style.textAlign = 'center';
|
||||
if (value.color) {
|
||||
style.color = value.color;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width, height }}>
|
||||
<div style={style}>{value.text}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
getProcessedValues = (): DisplayValue[] => {
|
||||
return getSingleStatValues(this.props);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { height, width, options, panelData } = this.props;
|
||||
const { orientation } = options;
|
||||
return (
|
||||
<ProcessedValuesRepeater
|
||||
getProcessedValues={this.getProcessedValues}
|
||||
renderValue={this.renderValue}
|
||||
width={width}
|
||||
height={height}
|
||||
source={panelData}
|
||||
orientation={orientation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{opacity:0.26;fill:url(#SVGID_1_);}
|
||||
.st1{fill:url(#SVGID_2_);}
|
||||
.st2{fill:url(#SVGID_3_);}
|
||||
.st3{fill:url(#SVGID_4_);}
|
||||
.st4{fill:url(#SVGID_5_);}
|
||||
.st5{fill:none;stroke:url(#SVGID_6_);stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="50" y1="65.6698" x2="50" y2="93.5681">
|
||||
<stop offset="0" style="stop-color:#FFF23A"/>
|
||||
<stop offset="4.010540e-02" style="stop-color:#FEE62D"/>
|
||||
<stop offset="0.1171" style="stop-color:#FED41A"/>
|
||||
<stop offset="0.1964" style="stop-color:#FDC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC60B"/>
|
||||
<stop offset="0.6685" style="stop-color:#F28F3F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED693C"/>
|
||||
<stop offset="1" style="stop-color:#E83E39"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M97.6,83.8H2.4c-1.3,0-2.4-1.1-2.4-2.4v-1.8l17-1l19.2-4.3l16.3-1.6l16.5,0l15.8-4.7l15.1-3v16.3
|
||||
C100,82.8,98.9,83.8,97.6,83.8z"/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="19.098" y1="76.0776" x2="19.098" y2="27.8027">
|
||||
<stop offset="0" style="stop-color:#FFF23A"/>
|
||||
<stop offset="4.010540e-02" style="stop-color:#FEE62D"/>
|
||||
<stop offset="0.1171" style="stop-color:#FED41A"/>
|
||||
<stop offset="0.1964" style="stop-color:#FDC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC60B"/>
|
||||
<stop offset="0.6685" style="stop-color:#F28F3F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED693C"/>
|
||||
<stop offset="1" style="stop-color:#E83E39"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M19.6,64.3V38.9l-5.2,3.9l-3.5-6l9.4-6.9h6.8v34.4H19.6z"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="42.412" y1="76.0776" x2="42.412" y2="27.8027">
|
||||
<stop offset="0" style="stop-color:#FFF23A"/>
|
||||
<stop offset="4.010540e-02" style="stop-color:#FEE62D"/>
|
||||
<stop offset="0.1171" style="stop-color:#FED41A"/>
|
||||
<stop offset="0.1964" style="stop-color:#FDC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC60B"/>
|
||||
<stop offset="0.6685" style="stop-color:#F28F3F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED693C"/>
|
||||
<stop offset="1" style="stop-color:#E83E39"/>
|
||||
</linearGradient>
|
||||
<path class="st2" d="M53.1,39.4c0,1.1-0.1,2.2-0.4,3.2c-0.3,1-0.7,1.9-1.2,2.8c-0.5,0.9-1,1.7-1.7,2.5c-0.6,0.8-1.2,1.6-1.9,2.3
|
||||
l-6.4,7.4h11.1v6.7H32.3v-6.9l10.5-12c0.8-1,1.5-2,2-3c0.5-1,0.7-2,0.7-2.9c0-1-0.2-1.9-0.7-2.6c-0.5-0.7-1.2-1.1-2.2-1.1
|
||||
c-0.9,0-1.7,0.4-2.3,1.1c-0.6,0.8-1,1.9-1.1,3.3l-7.3-0.7c0.4-3.5,1.6-6.1,3.6-7.9c2-1.7,4.5-2.6,7.4-2.6c1.6,0,3,0.2,4.3,0.7
|
||||
c1.3,0.5,2.3,1.2,3.2,2c0.9,0.9,1.6,1.9,2.1,3.2C52.8,36.4,53.1,37.8,53.1,39.4z"/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="60.3739" y1="76.0776" x2="60.3739" y2="27.8027">
|
||||
<stop offset="0" style="stop-color:#FFF23A"/>
|
||||
<stop offset="4.010540e-02" style="stop-color:#FEE62D"/>
|
||||
<stop offset="0.1171" style="stop-color:#FED41A"/>
|
||||
<stop offset="0.1964" style="stop-color:#FDC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC60B"/>
|
||||
<stop offset="0.6685" style="stop-color:#F28F3F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED693C"/>
|
||||
<stop offset="1" style="stop-color:#E83E39"/>
|
||||
</linearGradient>
|
||||
<path class="st3" d="M64.5,60.4c0,1.2-0.4,2.3-1.2,3.1c-0.8,0.8-1.8,1.3-3,1.3c-1.2,0-2.2-0.4-3-1.3c-0.8-0.8-1.1-1.9-1.1-3.1
|
||||
c0-1.2,0.4-2.2,1.1-3.1c0.8-0.9,1.8-1.3,3-1.3c1.2,0,2.2,0.4,3,1.3C64.1,58.1,64.5,59.2,64.5,60.4z"/>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="77.5234" y1="76.0776" x2="77.5234" y2="27.8027">
|
||||
<stop offset="0" style="stop-color:#FFF23A"/>
|
||||
<stop offset="4.010540e-02" style="stop-color:#FEE62D"/>
|
||||
<stop offset="0.1171" style="stop-color:#FED41A"/>
|
||||
<stop offset="0.1964" style="stop-color:#FDC90F"/>
|
||||
<stop offset="0.2809" style="stop-color:#FDC60B"/>
|
||||
<stop offset="0.6685" style="stop-color:#F28F3F"/>
|
||||
<stop offset="0.8876" style="stop-color:#ED693C"/>
|
||||
<stop offset="1" style="stop-color:#E83E39"/>
|
||||
</linearGradient>
|
||||
<path class="st4" d="M85.5,57.4v6.9h-6.9v-6.9H66v-6.6l10.1-20.9h9.4V51H89v6.4H85.5z M78.8,37.5L78.8,37.5l-6,13.5h6V37.5z"/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-2.852199e-02" y1="72.3985" x2="100.0976" y2="72.3985">
|
||||
<stop offset="0" style="stop-color:#F28F3F"/>
|
||||
<stop offset="1" style="stop-color:#F28F3F"/>
|
||||
</linearGradient>
|
||||
<polyline class="st5" points="0,79.7 17,78.7 36.2,74.4 52.5,72.8 69,72.9 84.9,68.1 100,65.1 "/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
29
public/app/plugins/panel/singlestat2/module.tsx
Normal file
29
public/app/plugins/panel/singlestat2/module.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
|
||||
import { SingleStatPanel } from './SingleStatPanel';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { SingleStatEditor } from './SingleStatEditor';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel);
|
||||
|
||||
const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
|
||||
|
||||
export const singleStatBaseOptionsCheck = (
|
||||
options: Partial<SingleStatBaseOptions>,
|
||||
prevPluginId?: string,
|
||||
prevOptions?: any
|
||||
) => {
|
||||
if (prevOptions) {
|
||||
optionsToKeep.forEach(v => {
|
||||
if (prevOptions.hasOwnProperty(v)) {
|
||||
options[v] = cloneDeep(prevOptions.display);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
reactPanel.setEditor(SingleStatEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
|
20
public/app/plugins/panel/singlestat2/plugin.json
Normal file
20
public/app/plugins/panel/singlestat2/plugin.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Singlestat (react)",
|
||||
"id": "singlestat2",
|
||||
"state": "alpha",
|
||||
|
||||
"dataFormats": ["time_series", "table"],
|
||||
|
||||
"info": {
|
||||
"description": "Singlestat Panel for Grafana",
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icn-singlestat-panel.svg",
|
||||
"large": "img/icn-singlestat-panel.svg"
|
||||
}
|
||||
}
|
||||
}
|
33
public/app/plugins/panel/singlestat2/types.ts
Normal file
33
public/app/plugins/panel/singlestat2/types.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { VizOrientation, ValueMapping, Threshold } from '@grafana/ui';
|
||||
|
||||
export interface SingleStatBaseOptions {
|
||||
valueMappings: ValueMapping[];
|
||||
thresholds: Threshold[];
|
||||
valueOptions: SingleStatValueOptions;
|
||||
orientation: VizOrientation;
|
||||
}
|
||||
|
||||
export interface SingleStatValueOptions {
|
||||
unit: string;
|
||||
suffix: string;
|
||||
stat: string;
|
||||
prefix: string;
|
||||
decimals?: number | null;
|
||||
}
|
||||
|
||||
export interface SingleStatOptions extends SingleStatBaseOptions {
|
||||
// TODO, fill in with options from angular
|
||||
}
|
||||
|
||||
export const defaults: SingleStatOptions = {
|
||||
valueOptions: {
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
decimals: null,
|
||||
stat: 'avg',
|
||||
unit: 'none',
|
||||
},
|
||||
valueMappings: [],
|
||||
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
|
||||
orientation: VizOrientation.Auto,
|
||||
};
|
Loading…
Reference in New Issue
Block a user