From 533b938fcd330bd920fad6a2c55ae8c38083534f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 08:19:40 +0100 Subject: [PATCH 01/12] Removed baseColor --- .../ThresholdsEditor/ThresholdsEditor.tsx | 21 +++++---- .../plugins/panel/gauge/GaugePanelOptions.tsx | 2 - public/app/plugins/panel/gauge/types.ts | 1 - public/app/viz/Gauge.test.tsx | 9 ++-- public/app/viz/Gauge.tsx | 47 +++++++++---------- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 6e8d88051f3..bd4f83f4dba 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -19,9 +19,15 @@ export class ThresholdsEditor extends PureComponent { constructor(props: Props) { super(props); - const thresholds: Threshold[] = - props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }]; + const addDefaultThreshold = this.props.thresholds.length === 0; + const thresholds: Threshold[] = addDefaultThreshold + ? [{ index: 0, value: -Infinity, color: colors[0] }] + : props.thresholds; this.state = { thresholds }; + + if (addDefaultThreshold) { + this.onChange(); + } } onAddThreshold = (index: number) => { @@ -62,7 +68,7 @@ export class ThresholdsEditor extends PureComponent { }, ]), }, - () => this.updateGauge() + () => this.onChange() ); }; @@ -85,7 +91,7 @@ export class ThresholdsEditor extends PureComponent { thresholds: newThresholds.filter(t => t !== threshold), }; }, - () => this.updateGauge() + () => this.onChange() ); }; @@ -124,11 +130,10 @@ export class ThresholdsEditor extends PureComponent { { thresholds: newThresholds, }, - () => this.updateGauge() + () => this.onChange() ); }; - onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds); onBlur = () => { this.setState(prevState => { const sortThresholds = this.sortThresholds([...prevState.thresholds]); @@ -139,10 +144,10 @@ export class ThresholdsEditor extends PureComponent { return { thresholds: sortThresholds }; }); - this.updateGauge(); + this.onChange(); }; - updateGauge = () => { + onChange = () => { this.props.onChange(this.state.thresholds); }; diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx index 9729416b7e6..18a445d840d 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx @@ -1,6 +1,5 @@ import React, { PureComponent } from 'react'; import { - BasicGaugeColor, PanelOptionsProps, ThresholdsEditor, Threshold, @@ -15,7 +14,6 @@ import { GaugeOptions } from './types'; export const defaultProps = { options: { - baseColor: BasicGaugeColor.Green, minValue: 0, maxValue: 100, prefix: '', diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts index b698a3389c2..42262178dc8 100644 --- a/public/app/plugins/panel/gauge/types.ts +++ b/public/app/plugins/panel/gauge/types.ts @@ -1,7 +1,6 @@ import { Threshold, ValueMapping } from '@grafana/ui'; export interface GaugeOptions { - baseColor: string; decimals: number; valueMappings: ValueMapping[]; maxValue: number; diff --git a/public/app/viz/Gauge.test.tsx b/public/app/viz/Gauge.test.tsx index 69c7733f44b..2678b3f2ad1 100644 --- a/public/app/viz/Gauge.test.tsx +++ b/public/app/viz/Gauge.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { BasicGaugeColor, TimeSeriesVMs } from '@grafana/ui'; +import { TimeSeriesVMs } from '@grafana/ui'; import { Gauge, Props } from './Gauge'; @@ -10,7 +10,6 @@ jest.mock('jquery', () => ({ const setup = (propOverrides?: object) => { const props: Props = { - baseColor: BasicGaugeColor.Green, maxValue: 100, valueMappings: [], minValue: 0, @@ -18,7 +17,7 @@ const setup = (propOverrides?: object) => { showThresholdMarkers: true, showThresholdLabels: false, suffix: '', - thresholds: [], + thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }], unit: 'none', stat: 'avg', height: 300, @@ -42,12 +41,12 @@ describe('Get font color', () => { it('should get base color if no threshold', () => { const { instance } = setup(); - expect(instance.getFontColor(40)).toEqual(BasicGaugeColor.Green); + expect(instance.getFontColor(40)).toEqual('#7EB26D'); }); it('should be f2f2f2', () => { const { instance } = setup({ - thresholds: [{ value: 59, color: '#f2f2f2' }], + thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 59, color: '#f2f2f2' }], }); expect(instance.getFontColor(58)).toEqual('#f2f2f2'); diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 094e630a1c0..069a8dbcc3c 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -6,7 +6,6 @@ import config from '../core/config'; import kbn from '../core/utils/kbn'; export interface Props { - baseColor: string; decimals: number; height: number; valueMappings: ValueMapping[]; @@ -27,7 +26,6 @@ export class Gauge extends PureComponent { canvasElement: any; static defaultProps = { - baseColor: BasicGaugeColor.Green, maxValue: 100, valueMappings: [], minValue: 0, @@ -91,24 +89,25 @@ export class Gauge extends PureComponent { } getFontColor(value) { - const { baseColor, maxValue, thresholds } = this.props; + const { maxValue, thresholds } = this.props; - if (thresholds.length > 0) { - const atThreshold = thresholds.filter(threshold => value <= threshold.value); - - if (atThreshold.length > 0) { - return atThreshold[0].color; - } else if (value <= maxValue) { - return BasicGaugeColor.Red; - } + if (thresholds.length === 1) { + return thresholds[0].color; } - return baseColor; + const atThreshold = thresholds.filter(threshold => value < threshold.value); + + if (atThreshold.length > 0) { + return atThreshold[0].color; + } else if (value <= maxValue) { + return BasicGaugeColor.Red; + } + + return ''; } draw() { const { - baseColor, maxValue, minValue, timeSeries, @@ -137,16 +136,16 @@ export class Gauge extends PureComponent { const thresholdMarkersWidth = gaugeWidth / 5; const thresholdLabelFontSize = fontSize / 2.5; - const formattedThresholds = [ - { value: minValue, color: BasicGaugeColor.Green }, - ...thresholds.map((threshold, index) => { - return { - value: threshold.value, - color: index === 0 ? threshold.color : thresholds[index].color, - }; - }), - { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor }, - ]; + // const formattedThresholds = [ + // { value: minValue, color: BasicGaugeColor.Green }, + // ...thresholds.map((threshold, index) => { + // return { + // value: threshold.value, + // color: index === 0 ? threshold.color : thresholds[index].color, + // }; + // }), + // { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor }, + // ]; const options = { series: { @@ -164,7 +163,7 @@ export class Gauge extends PureComponent { layout: { margin: 0, thresholdWidth: 0 }, cell: { border: { width: 0 } }, threshold: { - values: formattedThresholds, + values: thresholds, label: { show: showThresholdLabels, margin: thresholdMarkersWidth + 1, From 9dcf3d58ea77cbee729fd26022ce83bdcc2358d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 09:54:31 +0100 Subject: [PATCH 02/12] Fixed getFontColor, added tests and fixed thresholds logic --- public/app/viz/Gauge.test.tsx | 28 ++++++++++++++++++++++------ public/app/viz/Gauge.tsx | 35 +++++++++++++++++------------------ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/public/app/viz/Gauge.test.tsx b/public/app/viz/Gauge.test.tsx index 2678b3f2ad1..3fed641c9a2 100644 --- a/public/app/viz/Gauge.test.tsx +++ b/public/app/viz/Gauge.test.tsx @@ -38,17 +38,33 @@ const setup = (propOverrides?: object) => { }; describe('Get font color', () => { - it('should get base color if no threshold', () => { - const { instance } = setup(); + it('should get first threshold color when only one threshold', () => { + const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] }); - expect(instance.getFontColor(40)).toEqual('#7EB26D'); + expect(instance.getFontColor(49)).toEqual('#7EB26D'); }); - it('should be f2f2f2', () => { + it('should get the next threshold color if value is same as a threshold', () => { const { instance } = setup({ - thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 59, color: '#f2f2f2' }], + thresholds: [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ], }); - expect(instance.getFontColor(58)).toEqual('#f2f2f2'); + expect(instance.getFontColor(50)).toEqual('#6ED0E0'); + }); + + it('should get the nearest threshold color', () => { + 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(6.5)).toEqual('#EAB839'); }); }); diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 069a8dbcc3c..75ad799d322 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -82,14 +82,14 @@ export class Gauge extends PureComponent { } if (isNaN(value)) { - return '-'; + return value; } return `${prefix} ${formattedValue} ${suffix}`; } - getFontColor(value) { - const { maxValue, thresholds } = this.props; + getFontColor(value: string | number) { + const { thresholds } = this.props; if (thresholds.length === 1) { return thresholds[0].color; @@ -98,12 +98,11 @@ export class Gauge extends PureComponent { const atThreshold = thresholds.filter(threshold => value < threshold.value); if (atThreshold.length > 0) { - return atThreshold[0].color; - } else if (value <= maxValue) { - return BasicGaugeColor.Red; + const nearestThreshold = atThreshold.sort((t1, t2) => t1.value - t2.value)[0]; + return nearestThreshold.color; } - return ''; + return BasicGaugeColor.Red; } draw() { @@ -136,16 +135,16 @@ export class Gauge extends PureComponent { const thresholdMarkersWidth = gaugeWidth / 5; const thresholdLabelFontSize = fontSize / 2.5; - // const formattedThresholds = [ - // { value: minValue, color: BasicGaugeColor.Green }, - // ...thresholds.map((threshold, index) => { - // return { - // value: threshold.value, - // color: index === 0 ? threshold.color : thresholds[index].color, - // }; - // }), - // { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor }, - // ]; + const formattedThresholds = [ + { value: minValue, color: thresholds.length === 1 ? thresholds[0].color : BasicGaugeColor.Green }, + ...thresholds.map((threshold, index) => { + return { + value: threshold.value, + color: thresholds[index].color, + }; + }), + { value: maxValue, color: thresholds.length === 1 ? thresholds[0].color : BasicGaugeColor.Red }, + ]; const options = { series: { @@ -163,7 +162,7 @@ export class Gauge extends PureComponent { layout: { margin: 0, thresholdWidth: 0 }, cell: { border: { width: 0 } }, threshold: { - values: thresholds, + values: formattedThresholds, label: { show: showThresholdLabels, margin: thresholdMarkersWidth + 1, From 554d010332cf1aceecbfdad15f6ed54445a4264f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 11:01:49 +0100 Subject: [PATCH 03/12] Preparing move to ui/viz --- packages/grafana-ui/src/types/panel.ts | 7 +++++++ public/app/viz/Gauge.tsx | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 7e4012ad529..340bec9d37b 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -66,3 +66,10 @@ export interface RangeMap extends BaseMap { from: string; to: string; } + +export type Theme = 'dark' | 'light'; + +export enum Themes { + Dark = 'dark', + Light = 'light', +} diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 75ad799d322..1a611d79783 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -1,9 +1,15 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui'; - -import config from '../core/config'; -import kbn from '../core/utils/kbn'; +import { + BasicGaugeColor, + Threshold, + TimeSeriesVMs, + MappingType, + ValueMapping, + getValueFormat, + Theme, + Themes, +} from '@grafana/ui'; export interface Props { decimals: number; @@ -20,6 +26,7 @@ export interface Props { suffix: string; unit: string; width: number; + theme?: Theme; } export class Gauge extends PureComponent { @@ -68,7 +75,7 @@ export class Gauge extends PureComponent { formatValue(value) { const { decimals, valueMappings, prefix, suffix, unit } = this.props; - const formatFunc = kbn.valueFormats[unit]; + const formatFunc = getValueFormat(unit); const formattedValue = formatFunc(value, decimals); if (valueMappings.length > 0) { @@ -116,6 +123,7 @@ export class Gauge extends PureComponent { width, height, stat, + theme, } = this.props; let value: string | number = ''; @@ -127,7 +135,7 @@ export class Gauge extends PureComponent { } const dimension = Math.min(width, height * 1.3); - const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; + const backgroundColor = theme === Themes.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; const fontScale = parseInt('80', 10) / 100; const fontSize = Math.min(dimension / 5, 100) * fontScale; const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; From 9a01f3e5178a49b625496c4ca9d6eeada588c36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 11:55:22 +0100 Subject: [PATCH 04/12] Moved Gauge to ui/components --- .../src/components/Gauge}/Gauge.test.tsx | 2 +- .../grafana-ui/src/components/Gauge}/Gauge.tsx | 14 ++++---------- packages/grafana-ui/src/components/index.ts | 1 + public/app/plugins/panel/gauge/GaugePanel.tsx | 5 ++--- .../state => plugins/panel/gauge}/timeSeries.ts | 0 5 files changed, 8 insertions(+), 14 deletions(-) rename {public/app/viz => packages/grafana-ui/src/components/Gauge}/Gauge.test.tsx (97%) rename {public/app/viz => packages/grafana-ui/src/components/Gauge}/Gauge.tsx (95%) rename public/app/{viz/state => plugins/panel/gauge}/timeSeries.ts (100%) diff --git a/public/app/viz/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx similarity index 97% rename from public/app/viz/Gauge.test.tsx rename to packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 3fed641c9a2..999f3f581ab 100644 --- a/public/app/viz/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { TimeSeriesVMs } from '@grafana/ui'; import { Gauge, Props } from './Gauge'; +import { TimeSeriesVMs } from '../../types/series'; jest.mock('jquery', () => ({ plot: jest.fn(), diff --git a/public/app/viz/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx similarity index 95% rename from public/app/viz/Gauge.tsx rename to packages/grafana-ui/src/components/Gauge/Gauge.tsx index 1a611d79783..b658ef4e023 100644 --- a/public/app/viz/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,15 +1,9 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { - BasicGaugeColor, - Threshold, - TimeSeriesVMs, - MappingType, - ValueMapping, - getValueFormat, - Theme, - Themes, -} from '@grafana/ui'; + +import { ValueMapping, Threshold, Theme, MappingType, BasicGaugeColor, Themes } from '../../types/panel'; +import { TimeSeriesVMs } from '../../types/series'; +import { getValueFormat } from '../../utils/valueFormats/valueFormats'; export interface Props { decimals: number; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 88959bd8cb9..584992f8803 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -22,3 +22,4 @@ export { Graph } from './Graph/Graph'; export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor'; +export { Gauge } from './Gauge/Gauge'; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index cfce719b5a6..eb00caf55fb 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,8 +1,7 @@ import React, { PureComponent } from 'react'; -import { PanelProps, NullValueMode } from '@grafana/ui'; +import { PanelProps, NullValueMode, Gauge } from '@grafana/ui'; -import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; -import Gauge from 'app/viz/Gauge'; +import { getTimeSeriesVMs } from './timeSeries'; import { GaugeOptions } from './types'; interface Props extends PanelProps {} diff --git a/public/app/viz/state/timeSeries.ts b/public/app/plugins/panel/gauge/timeSeries.ts similarity index 100% rename from public/app/viz/state/timeSeries.ts rename to public/app/plugins/panel/gauge/timeSeries.ts From a6e2be862c099ead673373f7774189ab6c9e4b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 14:04:39 +0100 Subject: [PATCH 05/12] Added typings and refactored valuemappings code --- .../grafana-ui/src/components/Gauge/Gauge.tsx | 115 +++++++++++++----- packages/grafana-ui/src/types/series.ts | 5 +- .../grafana-ui/src/utils/processTimeSeries.ts | 4 +- public/app/plugins/panel/gauge/timeSeries.ts | 4 +- 4 files changed, 92 insertions(+), 36 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index b658ef4e023..aefd6ed7882 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,10 +1,21 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { ValueMapping, Threshold, Theme, MappingType, BasicGaugeColor, Themes } from '../../types/panel'; +import { + ValueMapping, + Threshold, + Theme, + MappingType, + BasicGaugeColor, + Themes, + ValueMap, + RangeMap, +} from '../../types/panel'; import { TimeSeriesVMs } from '../../types/series'; import { getValueFormat } from '../../utils/valueFormats/valueFormats'; +type TimeSeriesValue = string | number | null; + export interface Props { decimals: number; height: number; @@ -47,56 +58,100 @@ export class Gauge extends PureComponent { this.draw(); } - formatWithMappings(mappings, value) { - const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText); - const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText); + addValueToTextMappingText( + allTexts: Array<{ text: string; type: MappingType }>, + valueToTextMapping: ValueMap, + value: TimeSeriesValue + ) { + if (!valueToTextMapping.value) { + return allTexts; + } - const valueMap = valueMaps.map(mapping => { - if (mapping.value && value === mapping.value) { - return mapping.text; - } - })[0]; + const valueAsNumber = parseFloat(value as string); + const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string); - const rangeMap = rangeMaps.map(mapping => { - if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) { - return mapping.text; - } - })[0]; + if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) { + return allTexts; + } - return { rangeMap, valueMap }; + if (valueAsNumber !== valueToTextMappingAsNumber) { + return allTexts; + } + + return allTexts.concat({ text: valueToTextMapping.text, type: MappingType.ValueToText }); } - formatValue(value) { + addRangeToTextMappingText( + allTexts: Array<{ text: string; type: MappingType }>, + rangeToTextMapping: RangeMap, + value: TimeSeriesValue + ) { + if ( + rangeToTextMapping.from && + rangeToTextMapping.to && + value && + value >= rangeToTextMapping.from && + value <= rangeToTextMapping.to + ) { + return allTexts.concat({ text: rangeToTextMapping.text, type: MappingType.RangeToText }); + } + + return allTexts; + } + + getAllMappingTexts(valueMappings: ValueMapping[], value: TimeSeriesValue) { + const allMappingTexts = valueMappings.reduce( + (allTexts, valueMapping) => { + if (valueMapping.type === MappingType.ValueToText) { + allTexts = this.addValueToTextMappingText(allTexts, valueMapping as ValueMap, value); + } else if (valueMapping.type === MappingType.RangeToText) { + allTexts = this.addRangeToTextMappingText(allTexts, valueMapping as RangeMap, value); + } + + return allTexts; + }, + [] as Array<{ text: string; type: MappingType }> + ); + + allMappingTexts.sort((t1, t2) => { + return t1.type - t2.type; + }); + + return allMappingTexts; + } + + formatWithValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) { + return this.getAllMappingTexts(valueMappings, value)[0]; + } + + formatValue(value: TimeSeriesValue) { const { decimals, valueMappings, prefix, suffix, unit } = this.props; - const formatFunc = getValueFormat(unit); - const formattedValue = formatFunc(value, decimals); + if (isNaN(value as number)) { + return value; + } if (valueMappings.length > 0) { - const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue); - - if (valueMap) { - return `${prefix} ${valueMap} ${suffix}`; - } else if (rangeMap) { - return `${prefix} ${rangeMap} ${suffix}`; + const valueMappedValue = this.formatWithValueMappings(valueMappings, value); + if (valueMappedValue) { + return `${prefix} ${valueMappedValue.text} ${suffix}`; } } - if (isNaN(value)) { - return value; - } + const formatFunc = getValueFormat(unit); + const formattedValue = formatFunc(value as number, decimals); return `${prefix} ${formattedValue} ${suffix}`; } - getFontColor(value: string | number) { + getFontColor(value: TimeSeriesValue) { const { thresholds } = this.props; if (thresholds.length === 1) { return thresholds[0].color; } - const atThreshold = thresholds.filter(threshold => value < threshold.value); + const atThreshold = thresholds.filter(threshold => (value as number) < threshold.value); if (atThreshold.length > 0) { const nearestThreshold = atThreshold.sort((t1, t2) => t1.value - t2.value)[0]; @@ -120,7 +175,7 @@ export class Gauge extends PureComponent { theme, } = this.props; - let value: string | number = ''; + let value: TimeSeriesValue = ''; if (timeSeries[0]) { value = timeSeries[0].stats[stat]; diff --git a/packages/grafana-ui/src/types/series.ts b/packages/grafana-ui/src/types/series.ts index 49662e9872d..5cad1e4a72a 100644 --- a/packages/grafana-ui/src/types/series.ts +++ b/packages/grafana-ui/src/types/series.ts @@ -21,9 +21,12 @@ export interface TimeSeriesVM { color: string; data: TimeSeriesValue[][]; stats: TimeSeriesStats; + allIsNull: boolean; + allIsZero: boolean; } export interface TimeSeriesStats { + [key: string]: number | null; total: number | null; max: number | null; min: number | null; @@ -36,8 +39,6 @@ export interface TimeSeriesStats { range: number | null; timeStep: number; count: number; - allIsNull: boolean; - allIsZero: boolean; } export enum NullValueMode { diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index e92aaf0c1a6..7254354a21b 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -151,6 +151,8 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O data: result, label: label, color: colorPalette[colorIndex], + allIsZero, + allIsNull, stats: { total, min, @@ -164,8 +166,6 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O range, count, first, - allIsZero, - allIsNull, }, }; }); diff --git a/public/app/plugins/panel/gauge/timeSeries.ts b/public/app/plugins/panel/gauge/timeSeries.ts index 5f27974a33b..18054fe0d5e 100644 --- a/public/app/plugins/panel/gauge/timeSeries.ts +++ b/public/app/plugins/panel/gauge/timeSeries.ts @@ -145,6 +145,8 @@ export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSe data: result, label: label, color: colors[colorIndex], + allIsZero, + allIsNull, stats: { total, min, @@ -158,8 +160,6 @@ export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSe range, count, first, - allIsZero, - allIsNull, }, }; }); From 4f6e87bbbf1ea66dfd780956caaf09c15649c41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 14:48:20 +0100 Subject: [PATCH 06/12] Small refactor of Gauge and tests --- .../src/components/Gauge/Gauge.test.tsx | 78 +++++++++++++++++++ .../grafana-ui/src/components/Gauge/Gauge.tsx | 50 +++++------- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 999f3f581ab..84f6b921a38 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import { Gauge, Props } from './Gauge'; import { TimeSeriesVMs } from '../../types/series'; +import { ValueMapping, MappingType } from '../../types'; jest.mock('jquery', () => ({ plot: jest.fn(), @@ -68,3 +69,80 @@ describe('Get font color', () => { expect(instance.getFontColor(6.5)).toEqual('#EAB839'); }); }); + +describe('Format value with value mappings', () => { + it('should return undefined with no valuemappings', () => { + const valueMappings: ValueMapping[] = []; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result).toBeUndefined(); + }); + + it('should return undefined with no matching valuemappings', () => { + 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 }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result).toBeUndefined(); + }); + + it('should return first matching mapping with lowest id', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, + { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' }, + ]; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual('1-20'); + }); + + it('should return rangeToText mapping where value equals to', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual('1-10'); + }); + + it('should return rangeToText mapping where value equals from', () => { + const valueMappings: ValueMapping[] = [ + { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' }, + { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, + ]; + const value = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual('10-20'); + }); + + it('should return rangeToText mapping where value is between from and to', () => { + 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 = 10; + const { instance } = setup({ valueMappings }); + + const result = instance.getFirstFormattedValueMapping(valueMappings, value); + + expect(result.text).toEqual('1-20'); + }); +}); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index aefd6ed7882..6a219a580c7 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -58,34 +58,26 @@ export class Gauge extends PureComponent { this.draw(); } - addValueToTextMappingText( - allTexts: Array<{ text: string; type: MappingType }>, - valueToTextMapping: ValueMap, - value: TimeSeriesValue - ) { + addValueToTextMappingText(allValueMappings: ValueMapping[], valueToTextMapping: ValueMap, value: TimeSeriesValue) { if (!valueToTextMapping.value) { - return allTexts; + return allValueMappings; } const valueAsNumber = parseFloat(value as string); const valueToTextMappingAsNumber = parseFloat(valueToTextMapping.value as string); if (isNaN(valueAsNumber) || isNaN(valueToTextMappingAsNumber)) { - return allTexts; + return allValueMappings; } if (valueAsNumber !== valueToTextMappingAsNumber) { - return allTexts; + return allValueMappings; } - return allTexts.concat({ text: valueToTextMapping.text, type: MappingType.ValueToText }); + return allValueMappings.concat(valueToTextMapping); } - addRangeToTextMappingText( - allTexts: Array<{ text: string; type: MappingType }>, - rangeToTextMapping: RangeMap, - value: TimeSeriesValue - ) { + addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) { if ( rangeToTextMapping.from && rangeToTextMapping.to && @@ -93,35 +85,35 @@ export class Gauge extends PureComponent { value >= rangeToTextMapping.from && value <= rangeToTextMapping.to ) { - return allTexts.concat({ text: rangeToTextMapping.text, type: MappingType.RangeToText }); + return allValueMappings.concat(rangeToTextMapping); } - return allTexts; + return allValueMappings; } - getAllMappingTexts(valueMappings: ValueMapping[], value: TimeSeriesValue) { - const allMappingTexts = valueMappings.reduce( - (allTexts, valueMapping) => { + getAllFormattedValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) { + const allFormattedValueMappings = valueMappings.reduce( + (allValueMappings, valueMapping) => { if (valueMapping.type === MappingType.ValueToText) { - allTexts = this.addValueToTextMappingText(allTexts, valueMapping as ValueMap, value); + allValueMappings = this.addValueToTextMappingText(allValueMappings, valueMapping as ValueMap, value); } else if (valueMapping.type === MappingType.RangeToText) { - allTexts = this.addRangeToTextMappingText(allTexts, valueMapping as RangeMap, value); + allValueMappings = this.addRangeToTextMappingText(allValueMappings, valueMapping as RangeMap, value); } - return allTexts; + return allValueMappings; }, - [] as Array<{ text: string; type: MappingType }> + [] as ValueMapping[] ); - allMappingTexts.sort((t1, t2) => { - return t1.type - t2.type; + allFormattedValueMappings.sort((t1, t2) => { + return t1.id - t2.id; }); - return allMappingTexts; + return allFormattedValueMappings; } - formatWithValueMappings(valueMappings: ValueMapping[], value: TimeSeriesValue) { - return this.getAllMappingTexts(valueMappings, value)[0]; + getFirstFormattedValueMapping(valueMappings: ValueMapping[], value: TimeSeriesValue) { + return this.getAllFormattedValueMappings(valueMappings, value)[0]; } formatValue(value: TimeSeriesValue) { @@ -132,7 +124,7 @@ export class Gauge extends PureComponent { } if (valueMappings.length > 0) { - const valueMappedValue = this.formatWithValueMappings(valueMappings, value); + const valueMappedValue = this.getFirstFormattedValueMapping(valueMappings, value); if (valueMappedValue) { return `${prefix} ${valueMappedValue.text} ${suffix}`; } From 8ccf212f343cfe77c9184ca7ce4d40a722df6624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 17 Jan 2019 15:14:07 +0100 Subject: [PATCH 07/12] Added tests for formatted value --- .../src/components/Gauge/Gauge.test.tsx | 60 +++++++++++++++++-- .../grafana-ui/src/components/Gauge/Gauge.tsx | 20 ++++--- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 84f6b921a38..f8f545694dc 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -73,7 +73,7 @@ describe('Get font color', () => { describe('Format value with value mappings', () => { it('should return undefined with no valuemappings', () => { const valueMappings: ValueMapping[] = []; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -86,7 +86,7 @@ describe('Format value with value mappings', () => { { 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 value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -99,7 +99,7 @@ describe('Format value with value mappings', () => { { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' }, { id: 1, operator: '', text: 'tio', type: MappingType.ValueToText, value: '10' }, ]; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -112,7 +112,7 @@ describe('Format value with value mappings', () => { { id: 0, operator: '', text: '1-10', type: MappingType.RangeToText, from: '1', to: '10' }, { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, ]; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -125,7 +125,7 @@ describe('Format value with value mappings', () => { { id: 0, operator: '', text: '10-20', type: MappingType.RangeToText, from: '10', to: '20' }, { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' }, ]; - const value = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -138,7 +138,7 @@ describe('Format value with value mappings', () => { { 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 = 10; + const value = '10'; const { instance } = setup({ valueMappings }); const result = instance.getFirstFormattedValueMapping(valueMappings, value); @@ -146,3 +146,51 @@ describe('Format value with value mappings', () => { expect(result.text).toEqual('1-20'); }); }); + +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 '); + }); +}); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 6a219a580c7..c590b1ad9b7 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -78,13 +78,19 @@ export class Gauge extends PureComponent { } addRangeToTextMappingText(allValueMappings: ValueMapping[], rangeToTextMapping: RangeMap, value: TimeSeriesValue) { - if ( - rangeToTextMapping.from && - rangeToTextMapping.to && - value && - value >= rangeToTextMapping.from && - value <= rangeToTextMapping.to - ) { + if (!rangeToTextMapping.from || !rangeToTextMapping.to || !value) { + return allValueMappings; + } + + const valueAsNumber = parseFloat(value as string); + const fromAsNumber = parseFloat(rangeToTextMapping.from as string); + const toAsNumber = parseFloat(rangeToTextMapping.to as string); + + if (isNaN(valueAsNumber) || isNaN(fromAsNumber) || isNaN(toAsNumber)) { + return allValueMappings; + } + + if (valueAsNumber >= fromAsNumber && valueAsNumber <= toAsNumber) { return allValueMappings.concat(rangeToTextMapping); } From 5448b72f7ca1c58dcee38213ccf7453156624e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 18 Jan 2019 06:57:00 +0100 Subject: [PATCH 08/12] Passed the theme to Gauge --- public/app/plugins/panel/gauge/GaugePanel.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index eb00caf55fb..52ee273ef21 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,12 +1,17 @@ import React, { PureComponent } from 'react'; -import { PanelProps, NullValueMode, Gauge } from '@grafana/ui'; +import { PanelProps, NullValueMode, Gauge, Themes } from '@grafana/ui'; import { getTimeSeriesVMs } from './timeSeries'; import { GaugeOptions } from './types'; +import { contextSrv } from 'app/core/core'; interface Props extends PanelProps {} export class GaugePanel extends PureComponent { + getTheme() { + return contextSrv.user.lightTheme ? Themes.Light : Themes.Dark; + } + render() { const { timeSeries, width, height, onInterpolate, options } = this.props; @@ -26,6 +31,7 @@ export class GaugePanel extends PureComponent { height={height} prefix={prefix} suffix={suffix} + theme={this.getTheme()} /> ); } From c17ccf2289945792c1a399bf56c590b169c82baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 18 Jan 2019 07:10:00 +0100 Subject: [PATCH 09/12] Make sure we do not change -Infinity --- .../src/components/ThresholdsEditor/ThresholdsEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index bd4f83f4dba..590aca5c7a1 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -105,7 +105,7 @@ export class ThresholdsEditor extends PureComponent { const value = isNaN(parsedValue) ? null : parsedValue; const newThresholds = thresholds.map(t => { - if (t === threshold) { + if (t === threshold && t.index !== 0) { t = { ...t, value: value as number }; } From 4cc0be2568148b8d480b626ecd271cf1530be3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 18 Jan 2019 09:54:25 +0100 Subject: [PATCH 10/12] Redid logic for fontcolor and thresholds in Gauge and added tests --- .../src/components/Gauge/Gauge.test.tsx | 36 +++++++++++++-- .../grafana-ui/src/components/Gauge/Gauge.tsx | 46 ++++++++++++------- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index f8f545694dc..b3396841d4d 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -45,7 +45,7 @@ describe('Get font color', () => { expect(instance.getFontColor(49)).toEqual('#7EB26D'); }); - it('should get the next threshold color if value is same as a threshold', () => { + it('should get the threshold color if value is same as a threshold', () => { const { instance } = setup({ thresholds: [ { index: 2, value: 75, color: '#6ED0E0' }, @@ -54,10 +54,10 @@ describe('Get font color', () => { ], }); - expect(instance.getFontColor(50)).toEqual('#6ED0E0'); + expect(instance.getFontColor(50)).toEqual('#EAB839'); }); - it('should get the nearest threshold color', () => { + it('should get the nearest threshold color between thresholds', () => { const { instance } = setup({ thresholds: [ { index: 2, value: 75, color: '#6ED0E0' }, @@ -66,7 +66,35 @@ describe('Get font color', () => { ], }); - expect(instance.getFontColor(6.5)).toEqual('#EAB839'); + 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' }] }); + + expect(instance.getFormattedThresholds()).toEqual([ + { value: 0, color: '#7EB26D' }, + { value: 100, color: '#7EB26D' }, + ]); + }); + + it('should get the correct formatted values when thresholds are added', () => { + const { instance } = setup({ + thresholds: [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ], + }); + + expect(instance.getFormattedThresholds()).toEqual([ + { value: 0, color: '#7EB26D' }, + { value: 50, color: '#7EB26D' }, + { value: 75, color: '#EAB839' }, + { value: 100, color: '#6ED0E0' }, + ]); }); }); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index c590b1ad9b7..8013387812a 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -149,16 +149,42 @@ export class Gauge extends PureComponent { return thresholds[0].color; } - const atThreshold = thresholds.filter(threshold => (value as number) < threshold.value); + const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0]; + if (atThreshold) { + return atThreshold.color; + } - if (atThreshold.length > 0) { - const nearestThreshold = atThreshold.sort((t1, t2) => t1.value - t2.value)[0]; + const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value); + + if (belowThreshold.length > 0) { + const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; return nearestThreshold.color; } return BasicGaugeColor.Red; } + getFormattedThresholds() { + const { maxValue, minValue, thresholds } = this.props; + + const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index); + const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1]; + + const formattedThresholds = [ + ...thresholdsSortedByIndex.map(threshold => { + if (threshold.index === 0) { + return { value: minValue, color: threshold.color }; + } + + const previousThreshold = thresholdsSortedByIndex[threshold.index - 1]; + return { value: threshold.value, color: previousThreshold.color }; + }), + { value: maxValue, color: lastThreshold.color }, + ]; + + return formattedThresholds; + } + draw() { const { maxValue, @@ -166,7 +192,6 @@ export class Gauge extends PureComponent { timeSeries, showThresholdLabels, showThresholdMarkers, - thresholds, width, height, stat, @@ -190,17 +215,6 @@ export class Gauge extends PureComponent { const thresholdMarkersWidth = gaugeWidth / 5; const thresholdLabelFontSize = fontSize / 2.5; - const formattedThresholds = [ - { value: minValue, color: thresholds.length === 1 ? thresholds[0].color : BasicGaugeColor.Green }, - ...thresholds.map((threshold, index) => { - return { - value: threshold.value, - color: thresholds[index].color, - }; - }), - { value: maxValue, color: thresholds.length === 1 ? thresholds[0].color : BasicGaugeColor.Red }, - ]; - const options = { series: { gauges: { @@ -217,7 +231,7 @@ export class Gauge extends PureComponent { layout: { margin: 0, thresholdWidth: 0 }, cell: { border: { width: 0 } }, threshold: { - values: formattedThresholds, + values: this.getFormattedThresholds(), label: { show: showThresholdLabels, margin: thresholdMarkersWidth + 1, From f16101101d2035f6e5ce1f06f4e40efe8748fae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 11:58:29 +0100 Subject: [PATCH 11/12] Minor refactoring and name changes --- .../grafana-ui/src/components/Gauge/Gauge.tsx | 9 +- packages/grafana-ui/src/types/panel.ts | 4 +- .../grafana-ui/src/utils/processTimeSeries.ts | 9 +- public/app/core/services/context_srv.ts | 5 + public/app/plugins/panel/gauge/GaugePanel.tsx | 20 ++- public/app/plugins/panel/gauge/timeSeries.ts | 168 ------------------ .../app/plugins/panel/graph2/GraphPanel.tsx | 2 - 7 files changed, 29 insertions(+), 188 deletions(-) delete mode 100644 public/app/plugins/panel/gauge/timeSeries.ts diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 8013387812a..63d875e9cd5 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -4,10 +4,10 @@ import $ from 'jquery'; import { ValueMapping, Threshold, - Theme, + ThemeName, MappingType, BasicGaugeColor, - Themes, + ThemeNames, ValueMap, RangeMap, } from '../../types/panel'; @@ -31,7 +31,7 @@ export interface Props { suffix: string; unit: string; width: number; - theme?: Theme; + theme?: ThemeName; } export class Gauge extends PureComponent { @@ -48,6 +48,7 @@ export class Gauge extends PureComponent { thresholds: [], unit: 'none', stat: 'avg', + theme: ThemeNames.Dark, }; componentDidMount() { @@ -207,7 +208,7 @@ export class Gauge extends PureComponent { } const dimension = Math.min(width, height * 1.3); - const backgroundColor = theme === Themes.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; + const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; const fontScale = parseInt('80', 10) / 100; const fontSize = Math.min(dimension / 5, 100) * fontScale; const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 340bec9d37b..881bf920c27 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -67,9 +67,9 @@ export interface RangeMap extends BaseMap { to: string; } -export type Theme = 'dark' | 'light'; +export type ThemeName = 'dark' | 'light'; -export enum Themes { +export enum ThemeNames { Dark = 'dark', Light = 'light', } diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 7254354a21b..7b0c8e55239 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -1,18 +1,19 @@ // Libraries import _ from 'lodash'; +import { colors } from './colors'; + // Types import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; interface Options { timeSeries: TimeSeries[]; nullValueMode: NullValueMode; - colorPalette: string[]; } -export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: Options): TimeSeriesVMs { +export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { const vmSeries = timeSeries.map((item, index) => { - const colorIndex = index % colorPalette.length; + const colorIndex = index % colors.length; const label = item.target; const result = []; @@ -150,7 +151,7 @@ export function processTimeSeries({ timeSeries, nullValueMode, colorPalette }: O return { data: result, label: label, - color: colorPalette[colorIndex], + color: colors[colorIndex], allIsZero, allIsNull, stats: { diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index c4134598175..5353fb507cc 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -2,6 +2,7 @@ import config from 'app/core/config'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; import store from 'app/core/store'; +import { ThemeNames, ThemeName } from '@grafana/ui'; export class User { isGrafanaAdmin: any; @@ -59,6 +60,10 @@ export class ContextSrv { this.sidemenu = !this.sidemenu; store.set('grafana.sidemenu', this.sidemenu); } + + getTheme(): ThemeName { + return this.user.lightTheme ? ThemeNames.Light : ThemeNames.Dark; + } } const contextSrv = new ContextSrv(); diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 52ee273ef21..8b62171f31b 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -1,16 +1,20 @@ +// Libraries import React, { PureComponent } from 'react'; -import { PanelProps, NullValueMode, Gauge, Themes } from '@grafana/ui'; -import { getTimeSeriesVMs } from './timeSeries'; -import { GaugeOptions } from './types'; +// Services & Utils import { contextSrv } from 'app/core/core'; +import { processTimeSeries } from '@grafana/ui'; + +// Components +import { Gauge } from '@grafana/ui'; + +// Types +import { GaugeOptions } from './types'; +import { PanelProps, NullValueMode } from '@grafana/ui/src/types'; interface Props extends PanelProps {} export class GaugePanel extends PureComponent { - getTheme() { - return contextSrv.user.lightTheme ? Themes.Light : Themes.Dark; - } render() { const { timeSeries, width, height, onInterpolate, options } = this.props; @@ -18,7 +22,7 @@ export class GaugePanel extends PureComponent { const prefix = onInterpolate(options.prefix); const suffix = onInterpolate(options.suffix); - const vmSeries = getTimeSeriesVMs({ + const vmSeries = processTimeSeries({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, }); @@ -31,7 +35,7 @@ export class GaugePanel extends PureComponent { height={height} prefix={prefix} suffix={suffix} - theme={this.getTheme()} + theme={contextSrv.getTheme()} /> ); } diff --git a/public/app/plugins/panel/gauge/timeSeries.ts b/public/app/plugins/panel/gauge/timeSeries.ts deleted file mode 100644 index 18054fe0d5e..00000000000 --- a/public/app/plugins/panel/gauge/timeSeries.ts +++ /dev/null @@ -1,168 +0,0 @@ -// Libraries -import _ from 'lodash'; - -// Utils -import { colors } from '@grafana/ui'; - -// Types -import { TimeSeries, TimeSeriesVMs, NullValueMode } from '@grafana/ui'; - -interface Options { - timeSeries: TimeSeries[]; - nullValueMode: NullValueMode; -} - -export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { - const vmSeries = timeSeries.map((item, index) => { - const colorIndex = index % colors.length; - const label = item.target; - const result = []; - - // stat defaults - let total = 0; - let max = -Number.MAX_VALUE; - let min = Number.MAX_VALUE; - let logmin = Number.MAX_VALUE; - let avg = null; - let current = null; - let first = null; - let delta = 0; - let diff = null; - let range = null; - let timeStep = Number.MAX_VALUE; - let allIsNull = true; - let allIsZero = true; - - const ignoreNulls = nullValueMode === NullValueMode.Ignore; - const nullAsZero = nullValueMode === NullValueMode.AsZero; - - let currentTime; - let currentValue; - let nonNulls = 0; - let previousTime; - let previousValue = 0; - let previousDeltaUp = true; - - for (let i = 0; i < item.datapoints.length; i++) { - currentValue = item.datapoints[i][0]; - currentTime = item.datapoints[i][1]; - - // Due to missing values we could have different timeStep all along the series - // so we have to find the minimum one (could occur with aggregators such as ZimSum) - if (previousTime !== undefined) { - const currentStep = currentTime - previousTime; - if (currentStep < timeStep) { - timeStep = currentStep; - } - } - - previousTime = currentTime; - - if (currentValue === null) { - if (ignoreNulls) { - continue; - } - if (nullAsZero) { - currentValue = 0; - } - } - - if (currentValue !== null) { - if (_.isNumber(currentValue)) { - total += currentValue; - allIsNull = false; - nonNulls++; - } - - if (currentValue > max) { - max = currentValue; - } - - if (currentValue < min) { - min = currentValue; - } - - if (first === null) { - first = currentValue; - } else { - if (previousValue > currentValue) { - // counter reset - previousDeltaUp = false; - if (i === item.datapoints.length - 1) { - // reset on last - delta += currentValue; - } - } else { - if (previousDeltaUp) { - delta += currentValue - previousValue; // normal increment - } else { - delta += currentValue; // account for counter reset - } - previousDeltaUp = true; - } - } - previousValue = currentValue; - - if (currentValue < logmin && currentValue > 0) { - logmin = currentValue; - } - - if (currentValue !== 0) { - allIsZero = false; - } - } - - result.push([currentTime, currentValue]); - } - - if (max === -Number.MAX_VALUE) { - max = null; - } - - if (min === Number.MAX_VALUE) { - min = null; - } - - if (result.length && !allIsNull) { - avg = total / nonNulls; - current = result[result.length - 1][1]; - if (current === null && result.length > 1) { - current = result[result.length - 2][1]; - } - } - - if (max !== null && min !== null) { - range = max - min; - } - - if (current !== null && first !== null) { - diff = current - first; - } - - const count = result.length; - - return { - data: result, - label: label, - color: colors[colorIndex], - allIsZero, - allIsNull, - stats: { - total, - min, - max, - current, - logmin, - avg, - diff, - delta, - timeStep, - range, - count, - first, - }, - }; - }); - - return vmSeries; -} diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index 28c17dbad2c..2fef35b4f5f 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -1,7 +1,6 @@ // Libraries import _ from 'lodash'; import React, { PureComponent } from 'react'; -import { colors } from '@grafana/ui'; // Utils import { processTimeSeries } from '@grafana/ui/src/utils'; @@ -23,7 +22,6 @@ export class GraphPanel extends PureComponent { const vmSeries = processTimeSeries({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, - colorPalette: colors, }); return ( From 3a827fc2f128a366a30017728055af7e6f93fa47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Jan 2019 13:12:32 +0100 Subject: [PATCH 12/12] Added test case dashboard --- devenv/dev-dashboards/panel_tests_gauge.json | 1250 +++++++++++++++++ .../grafana-ui/src/utils/processTimeSeries.ts | 4 +- public/app/features/dashboard/panel_model.ts | 1 - public/app/plugins/panel/gauge/GaugePanel.tsx | 2 +- 4 files changed, 1253 insertions(+), 4 deletions(-) create mode 100644 devenv/dev-dashboards/panel_tests_gauge.json diff --git a/devenv/dev-dashboards/panel_tests_gauge.json b/devenv/dev-dashboards/panel_tests_gauge.json new file mode 100644 index 00000000000..c6e81ececc8 --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_gauge.json @@ -0,0 +1,1250 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "iteration": 1547810606599, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 11, + "panels": [], + "title": "Value options tests", + "type": "row" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 5, + "x": 0, + "y": 1 + }, + "id": 2, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Average, 2 decimals, ms unit", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 5, + "y": 1 + }, + "id": 5, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "max", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Max (90 ms), no decimals", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 5, + "x": 11, + "y": 1 + }, + "id": 6, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "p", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "s", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Current (10 ms), no unit, prefix (p), suffix (s)", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 4, + "w": 3, + "x": 16, + "y": 1 + }, + "id": 16, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 4, + "w": 5, + "x": 19, + "y": 1 + }, + "id": 18, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,91" + } + ], + "timeFrom": "1h", + "timeShift": null, + "title": "", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 4, + "w": 3, + "x": 16, + "y": 5 + }, + "id": 17, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 4, + "w": 5, + "x": 19, + "y": 5 + }, + "id": 19, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,81" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 15, + "panels": [], + "title": "Value Mappings", + "type": "row" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 4, + "x": 0, + "y": 10 + }, + "id": 12, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "", + "id": 1, + "operator": "", + "text": "TEN", + "to": "", + "type": 1, + "value": "10" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "value mapping 10 -> TEN", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "description": "should read N/A", + "gridPos": { + "h": 8, + "w": 4, + "x": 4, + "y": 10 + }, + "id": 13, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "", + "id": 1, + "operator": "", + "text": "N/A", + "to": "", + "type": 1, + "value": "null" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,null,null,null,null" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "value mapping null -> N/A", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "description": "should read N/A", + "gridPos": { + "h": 8, + "w": 6, + "x": 8, + "y": 10 + }, + "id": 20, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "0", + "id": 1, + "operator": "", + "text": "OK", + "to": "10", + "type": 2, + "value": "null" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,null,null,null,null,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "value mapping range, 0-10 -> OK, value 10", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "description": "should read N/A", + "gridPos": { + "h": 8, + "w": 6, + "x": 14, + "y": 10 + }, + "id": 21, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "current", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "0", + "id": 1, + "operator": "", + "text": "OK", + "to": "90", + "type": 2, + "value": "null" + }, + { + "from": "90", + "id": 2, + "operator": "", + "text": "BAD", + "to": "100", + "type": 2, + "value": "" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,null,null,null,null,10,95" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "value mapping range, 90-100 -> BAD, value 90", + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 9, + "panels": [], + "title": "Templating & Repeat", + "type": "row" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 19 + }, + "id": 7, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "$Servers", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "repeat": "Servers", + "repeatDirection": "h", + "scopedVars": { + "Servers": { + "selected": false, + "text": "server1", + "value": "server1" + } + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "repeat $Servers", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 19 + }, + "id": 22, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "$Servers", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "repeat": null, + "repeatDirection": "h", + "repeatIteration": 1547810606599, + "repeatPanelId": 7, + "scopedVars": { + "Servers": { + "selected": false, + "text": "server2", + "value": "server2" + } + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "repeat $Servers", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 19 + }, + "id": 23, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "$Servers", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "repeat": null, + "repeatDirection": "h", + "repeatIteration": 1547810606599, + "repeatPanelId": 7, + "scopedVars": { + "Servers": { + "selected": false, + "text": "server3", + "value": "server3" + } + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "repeat $Servers", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 19 + }, + "id": 24, + "links": [], + "nullPointMode": "null", + "options-gauge": { + "baseColor": "#299c46", + "decimals": "2", + "maxValue": 100, + "minValue": 0, + "options": { + "baseColor": "#299c46", + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "$Servers", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#e24d42", + "index": 2, + "value": 90 + }, + { + "color": "#ef843c", + "index": 1, + "value": 75 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "ms", + "valueMappings": [] + }, + "repeat": null, + "repeatDirection": "h", + "repeatIteration": 1547810606599, + "repeatPanelId": 7, + "scopedVars": { + "Servers": { + "selected": false, + "text": "server4", + "value": "server4" + } + }, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "repeat $Servers", + "type": "gauge" + } + ], + "refresh": false, + "schemaVersion": 17, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": true, + "tags": [], + "text": "All", + "value": [ + "$__all" + ] + }, + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "name": "Servers", + "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "selected": false, + "text": "server1", + "value": "server1" + }, + { + "selected": false, + "text": "server2", + "value": "server2" + }, + { + "selected": false, + "text": "server3", + "value": "server3" + }, + { + "selected": false, + "text": "server4", + "value": "server4" + } + ], + "query": "server1,server2,server3,server4", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Panel Tests - Gauge", + "uid": "_5rDmaQiz", + "version": 5 +} diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 7b0c8e55239..f5389f1b2bd 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -50,8 +50,8 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS continue; } - if (typeof currentValue !== 'number') { - continue; + if (currentValue !== null && typeof currentValue !== 'number') { + throw {message: 'Time series contains non number values'}; } // Due to missing values we could have different timeStep all along the series diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 2fec8e379dd..f60b207e015 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -52,7 +52,6 @@ const mustKeepProps: { [str: string]: boolean } = { hasRefreshed: true, events: true, cacheTimeout: true, - nullPointMode: true, cachedPluginOptions: true, transparent: true, }; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 8b62171f31b..cd92f697ced 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -24,7 +24,7 @@ export class GaugePanel extends PureComponent { const vmSeries = processTimeSeries({ timeSeries: timeSeries, - nullValueMode: NullValueMode.Ignore, + nullValueMode: NullValueMode.Null, }); return (