Merge pull request #15925 from ryantxu/reusable-formatting-options

make value processing/formatting more reusable
pull/16035/head
Torkel Ödegaard 6 years ago committed by GitHub
commit bfa54d2e26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
  2. 41
      packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
  3. 92
      packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
  4. 64
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  5. 157
      packages/grafana-ui/src/utils/displayValue.test.ts
  6. 145
      packages/grafana-ui/src/utils/displayValue.ts
  7. 1
      packages/grafana-ui/src/utils/index.ts
  8. 2
      public/app/features/plugins/built_in_plugins.ts
  9. 53
      public/app/plugins/panel/bargauge/BarGaugePanel.tsx
  10. 4
      public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
  11. 12
      public/app/plugins/panel/bargauge/module.tsx
  12. 17
      public/app/plugins/panel/bargauge/types.ts
  13. 52
      public/app/plugins/panel/gauge/GaugePanel.tsx
  14. 5
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  15. 12
      public/app/plugins/panel/gauge/module.tsx
  16. 17
      public/app/plugins/panel/gauge/types.ts
  17. 48
      public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx
  18. 9
      public/app/plugins/panel/singlestat2/README.md
  19. 48
      public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
  20. 66
      public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
  21. 0
      public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx
  22. 83
      public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg
  23. 29
      public/app/plugins/panel/singlestat2/module.tsx
  24. 20
      public/app/plugins/panel/singlestat2/plugin.json
  25. 33
      public/app/plugins/panel/singlestat2/types.ts

@ -11,16 +11,14 @@ 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: '',
suffix: '',
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }], thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
unit: 'none',
height: 300, height: 300,
width: 300, width: 300,
value: 25, value: {
decimals: 0, text: '25',
numeric: 25,
},
theme: getTheme(), theme: getTheme(),
orientation: VizOrientation.Horizontal, orientation: VizOrientation.Horizontal,
}; };

@ -3,26 +3,21 @@ import React, { PureComponent, CSSProperties } from 'react';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
// Utils // Utils
import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils'; import { getColorFromHexRgbOrName, getThresholdForValue, DisplayValue } from '../../utils';
// Types // Types
import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types'; import { Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
const BAR_SIZE_RATIO = 0.8; const BAR_SIZE_RATIO = 0.8;
export interface Props extends Themeable { export interface Props extends Themeable {
height: number; height: number;
unit: string;
width: number; width: number;
thresholds: Threshold[]; thresholds: Threshold[];
valueMappings: ValueMapping[]; value: DisplayValue;
value: TimeSeriesValue;
maxValue: number; maxValue: number;
minValue: number; minValue: number;
orientation: VizOrientation; orientation: VizOrientation;
prefix?: string;
suffix?: string;
decimals?: number;
} }
/* /*
@ -32,24 +27,18 @@ export class BarGauge extends PureComponent<Props> {
static defaultProps: Partial<Props> = { static defaultProps: Partial<Props> = {
maxValue: 100, maxValue: 100,
minValue: 0, minValue: 0,
value: 100, value: {
unit: 'none', text: '100',
numeric: 100,
},
orientation: VizOrientation.Horizontal, orientation: VizOrientation.Horizontal,
thresholds: [], thresholds: [],
valueMappings: [],
}; };
getNumericValue(): number {
if (Number.isFinite(this.props.value as number)) {
return this.props.value as number;
}
return 0;
}
getValueColors(): BarColors { getValueColors(): BarColors {
const { thresholds, theme, value } = this.props; const { thresholds, theme, value } = this.props;
const activeThreshold = getThresholdForValue(thresholds, value); const activeThreshold = getThresholdForValue(thresholds, value.numeric);
if (activeThreshold !== null) { if (activeThreshold !== null) {
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type); const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
@ -78,7 +67,7 @@ export class BarGauge extends PureComponent<Props> {
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type); const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
// if we are past real value the cell is not "on" // 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) return tinycolor(color)
.setAlpha(0.15) .setAlpha(0.15)
.toRgbString(); .toRgbString();
@ -217,18 +206,14 @@ export class BarGauge extends PureComponent<Props> {
} }
render() { 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 valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
const valueFormatted = formatFunc(numericValue, decimals);
const vertical = orientation === 'vertical'; const vertical = orientation === 'vertical';
return vertical return vertical
? this.renderVerticalBar(valueFormatted, valuePercent) ? this.renderVerticalBar(value.text, valuePercent)
: this.renderHorizontalLCD(valueFormatted, valuePercent); : this.renderHorizontalLCD(value.text, valuePercent);
} }
} }

@ -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 { ValueMapping, Threshold, GrafanaThemeType } from '../../types'; import { Threshold, GrafanaThemeType } from '../../types';
import { getMappedValue } from '../../utils/valueMappings'; import { getColorFromHexRgbOrName } from '../../utils';
import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
import { Themeable } from '../../index'; import { Themeable } from '../../index';
import { DisplayValue } from '../../utils/displayValue';
type GaugeValue = string | number | null;
export interface Props extends Themeable { export interface Props extends Themeable {
decimals?: number | null;
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;
unit: string;
width: number; width: number;
value: number; value: DisplayValue;
} }
const FONT_SCALE = 1; const FONT_SCALE = 1;
@ -32,15 +24,10 @@ export class Gauge extends PureComponent<Props> {
static defaultProps: Partial<Props> = { static defaultProps: Partial<Props> = {
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',
}; };
componentDidMount() { componentDidMount() {
@ -51,39 +38,6 @@ export class Gauge extends PureComponent<Props> {
this.draw(); 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() { getFormattedThresholds() {
const { maxValue, minValue, thresholds, theme } = this.props; const { maxValue, minValue, thresholds, theme } = this.props;
@ -112,15 +66,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) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1);
const thresholdLabelFontSize = fontSize / 2.5; const thresholdLabelFontSize = fontSize / 2.5;
const options: any = { const options: any = {
@ -149,9 +101,9 @@ export class Gauge extends PureComponent<Props> {
width: thresholdMarkersWidth, width: thresholdMarkersWidth,
}, },
value: { value: {
color: this.getFontColor(value), color: value.color,
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' },
}, },
@ -160,7 +112,7 @@ export class Gauge extends PureComponent<Props> {
}, },
}; };
const plotSeries = { data: [[0, value]] }; const plotSeries = { data: [[0, value.numeric]] };
try { try {
$.plot(this.canvasElement, [plotSeries], options); $.plot(this.canvasElement, [plotSeries], options);

@ -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');
});
});

@ -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 './namedColorsPalette';
export * from './thresholds'; export * from './thresholds';
export * from './string'; export * from './string';
export * from './displayValue';
export * from './deprecationWarning'; export * from './deprecationWarning';
export { getMappedValue } from './valueMappings'; 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 tablePanel from 'app/plugins/panel/table/module';
import * as table2Panel from 'app/plugins/panel/table2/module'; import * as table2Panel from 'app/plugins/panel/table2/module';
import * as singlestatPanel from 'app/plugins/panel/singlestat/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 gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
import * as gaugePanel from 'app/plugins/panel/gauge/module'; import * as gaugePanel from 'app/plugins/panel/gauge/module';
import * as barGaugePanel from 'app/plugins/panel/bargauge/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/table/module': tablePanel,
'app/plugins/panel/table2/module': table2Panel, 'app/plugins/panel/table2/module': table2Panel,
'app/plugins/panel/singlestat/module': singlestatPanel, 'app/plugins/panel/singlestat/module': singlestatPanel,
'app/plugins/panel/singlestat2/module': singlestatPanel2,
'app/plugins/panel/gettingstarted/module': gettingStartedPanel, 'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
'app/plugins/panel/gauge/module': gaugePanel, 'app/plugins/panel/gauge/module': gaugePanel,
'app/plugins/panel/bargauge/module': barGaugePanel, 'app/plugins/panel/bargauge/module': barGaugePanel,

@ -2,55 +2,46 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// Services & Utils // Services & Utils
import { processSingleStatPanelData } from '@grafana/ui'; import { DisplayValue, PanelProps, BarGauge } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
// Components
import { BarGauge, VizRepeater } from '@grafana/ui';
// Types // Types
import { BarGaugeOptions } from './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); export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
const suffix = replaceVariables(valueOptions.suffix); renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
const { options } = this.props;
return ( return (
<BarGauge <BarGauge
value={value.value as number | null} value={value}
width={width} width={width}
height={height} height={height}
prefix={prefix}
suffix={suffix}
orientation={options.orientation} orientation={options.orientation}
unit={valueOptions.unit}
decimals={valueOptions.decimals}
thresholds={options.thresholds} thresholds={options.thresholds}
valueMappings={options.valueMappings}
theme={config.theme} theme={config.theme}
/> />
); );
} };
render() {
const { panelData, options, width, height } = this.props;
const values = processSingleStatPanelData({ getProcessedValues = (): DisplayValue[] => {
panelData: panelData, return getSingleStatValues(this.props);
stat: options.valueOptions.stat, };
});
render() {
const { height, width, options, panelData } = this.props;
const { orientation } = options;
return ( return (
<VizRepeater height={height} width={width} values={values} orientation={options.orientation}> <ProcessedValuesRepeater
{({ vizHeight, vizWidth, value }) => this.renderBarGauge(value, vizWidth, vizHeight)} getProcessedValues={this.getProcessedValues}
</VizRepeater> renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
orientation={orientation}
/>
); );
} }
} }

@ -2,13 +2,13 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// Components // Components
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui'; import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
// Types // Types
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui'; import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
import { BarGaugeOptions, orientationOptions } from './types'; 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>> { export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
onThresholdsChanged = (thresholds: Threshold[]) => onThresholdsChanged = (thresholds: Threshold[]) =>

@ -3,18 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
import { BarGaugePanel } from './BarGaugePanel'; import { BarGaugePanel } from './BarGaugePanel';
import { BarGaugePanelEditor } from './BarGaugePanelEditor'; import { BarGaugePanelEditor } from './BarGaugePanelEditor';
import { BarGaugeOptions, defaults } from './types'; import { BarGaugeOptions, defaults } from './types';
import { singleStatBaseOptionsCheck } from '../singlestat2/module';
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel); export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel);
reactPanel.setEditor(BarGaugePanelEditor); reactPanel.setEditor(BarGaugePanelEditor);
reactPanel.setDefaults(defaults); reactPanel.setDefaults(defaults);
reactPanel.setPanelTypeChangedHook((options: BarGaugeOptions, prevPluginId?: string, prevOptions?: any) => { reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
if (prevOptions && prevOptions.valueOptions) {
options.valueOptions = prevOptions.valueOptions;
options.thresholds = prevOptions.thresholds;
options.maxValue = prevOptions.maxValue;
options.minValue = prevOptions.minValue;
}
return options;
});

@ -1,20 +1,17 @@
import { Threshold, SelectOptionItem, ValueMapping, VizOrientation } from '@grafana/ui'; import { VizOrientation, SelectOptionItem } from '@grafana/ui';
import { SingleStatValueOptions } from '../gauge/types';
export interface BarGaugeOptions { import { SingleStatBaseOptions } from '../singlestat2/types';
minValue: number;
maxValue: number;
orientation: VizOrientation;
valueOptions: SingleStatValueOptions;
valueMappings: ValueMapping[];
thresholds: Threshold[];
}
export const orientationOptions: SelectOptionItem[] = [ export const orientationOptions: SelectOptionItem[] = [
{ value: VizOrientation.Horizontal, label: 'Horizontal' }, { value: VizOrientation.Horizontal, label: 'Horizontal' },
{ value: VizOrientation.Vertical, label: 'Vertical' }, { value: VizOrientation.Vertical, label: 'Vertical' },
]; ];
export interface BarGaugeOptions extends SingleStatBaseOptions {
minValue: number;
maxValue: number;
}
export const defaults: BarGaugeOptions = { export const defaults: BarGaugeOptions = {
minValue: 0, minValue: 0,
maxValue: 100, maxValue: 100,

@ -2,37 +2,27 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// Services & Utils // Services & Utils
import { processSingleStatPanelData } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
// Components // Components
import { Gauge, VizRepeater } from '@grafana/ui'; import { Gauge } from '@grafana/ui';
// Types // Types
import { GaugeOptions } from './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<PanelProps<GaugeOptions>> {
renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
export class GaugePanel extends PureComponent<Props> { const { options } = this.props;
renderGauge(value: SingleStatValueInfo, width, height) {
const { replaceVariables, options } = this.props;
const { valueOptions } = options;
const prefix = replaceVariables(valueOptions.prefix);
const suffix = replaceVariables(valueOptions.suffix);
return ( return (
<Gauge <Gauge
value={value.value as number | null} 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}
@ -40,20 +30,24 @@ export class GaugePanel extends PureComponent<Props> {
theme={config.theme} theme={config.theme}
/> />
); );
} };
render() {
const { panelData, options, height, width } = this.props;
const values = processSingleStatPanelData({ getProcessedValues = (): DisplayValue[] => {
panelData: panelData, return getSingleStatValues(this.props);
stat: options.valueOptions.stat, };
});
render() {
const { height, width, options, panelData } = this.props;
const { orientation } = options;
return ( return (
<VizRepeater height={height} width={width} values={values} orientation={VizOrientation.Auto}> <ProcessedValuesRepeater
{({ vizHeight, vizWidth, value }) => this.renderGauge(value, vizWidth, vizHeight)} getProcessedValues={this.getProcessedValues}
</VizRepeater> renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
orientation={orientation}
/>
); );
} }
} }

@ -9,9 +9,10 @@ import {
ValueMapping, ValueMapping,
} from '@grafana/ui'; } from '@grafana/ui';
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
import { GaugeOptionsBox } from './GaugeOptionsBox'; 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>> { export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
onThresholdsChanged = (thresholds: Threshold[]) => onThresholdsChanged = (thresholds: Threshold[]) =>

@ -3,18 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
import { GaugePanelEditor } from './GaugePanelEditor'; import { GaugePanelEditor } from './GaugePanelEditor';
import { GaugePanel } from './GaugePanel'; import { GaugePanel } from './GaugePanel';
import { GaugeOptions, defaults } from './types'; import { GaugeOptions, defaults } from './types';
import { singleStatBaseOptionsCheck } from '../singlestat2/module';
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel); export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
reactPanel.setEditor(GaugePanelEditor); reactPanel.setEditor(GaugePanelEditor);
reactPanel.setDefaults(defaults); reactPanel.setDefaults(defaults);
reactPanel.setPanelTypeChangedHook((options: GaugeOptions, prevPluginId?: string, prevOptions?: any) => { reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
if (prevOptions && prevOptions.valueOptions) {
options.valueOptions = prevOptions.valueOptions;
options.thresholds = prevOptions.thresholds;
options.maxValue = prevOptions.maxValue;
options.minValue = prevOptions.minValue;
}
return options;
});

@ -1,21 +1,11 @@
import { Threshold, ValueMapping } from '@grafana/ui'; import { SingleStatBaseOptions } from '../singlestat2/types';
import { VizOrientation } from '@grafana/ui';
export interface GaugeOptions { export interface GaugeOptions extends SingleStatBaseOptions {
valueMappings: ValueMapping[];
maxValue: number; maxValue: number;
minValue: number; minValue: number;
showThresholdLabels: boolean; showThresholdLabels: boolean;
showThresholdMarkers: 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 = { export const defaults: GaugeOptions = {
@ -32,4 +22,5 @@ export const defaults: GaugeOptions = {
}, },
valueMappings: [], valueMappings: [],
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }], 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>
);
}
}

@ -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/)

@ -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} />
</>
);
}
}

@ -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

@ -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);

@ -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"
}
}
}

@ -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…
Cancel
Save