FieldConfig: add thresholds and color modes (#21273)

This commit is contained in:
Ryan McKinley
2019-12-28 17:32:58 -08:00
committed by GitHub
parent 36aad1c101
commit d9e9843a10
53 changed files with 1598 additions and 729 deletions
@@ -1,7 +1,7 @@
import { storiesOf } from '@storybook/react';
import { number, text } from '@storybook/addon-knobs';
import { BarGauge, Props, BarGaugeDisplayMode } from './BarGauge';
import { VizOrientation } from '@grafana/data';
import { VizOrientation, ThresholdsMode, Field, FieldType, getDisplayProcessor } from '@grafana/data';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
@@ -35,6 +35,23 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
threshold2Value,
} = getKnobs();
const field: Partial<Field> = {
type: FieldType.number,
config: {
min: minValue,
max: maxValue,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: threshold1Value, color: threshold1Color },
{ value: threshold2Value, color: threshold2Color },
],
},
},
};
field.display = getDisplayProcessor({ field });
const props: Props = {
theme: {} as any,
width: 300,
@@ -44,15 +61,10 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
title: title,
numeric: value,
},
minValue: minValue,
maxValue: maxValue,
orientation: VizOrientation.Vertical,
displayMode: BarGaugeDisplayMode.Basic,
thresholds: [
{ value: -Infinity, color: 'green' },
{ value: threshold1Value, color: threshold1Color },
{ value: threshold2Value, color: threshold2Color },
],
field: field.config!,
display: field.display!,
};
Object.assign(props, overrides);
@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DisplayValue } from '@grafana/data';
import { DisplayValue, VizOrientation, ThresholdsMode, Field, FieldType, getDisplayProcessor } from '@grafana/data';
import {
BarGauge,
Props,
@@ -11,29 +11,38 @@ import {
getValuePercent,
BarGaugeDisplayMode,
} from './BarGauge';
import { VizOrientation } from '@grafana/data';
import { getTheme } from '../../themes';
const green = '#73BF69';
const orange = '#FF9830';
function getProps(propOverrides?: Partial<Props>): Props {
const field: Partial<Field> = {
type: FieldType.number,
config: {
min: 0,
max: 100,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 70, color: 'orange' },
{ value: 90, color: 'red' },
],
},
},
};
const theme = getTheme();
field.display = getDisplayProcessor({ field, theme });
const props: Props = {
maxValue: 100,
minValue: 0,
displayMode: BarGaugeDisplayMode.Basic,
thresholds: [
{ value: -Infinity, color: 'green' },
{ value: 70, color: 'orange' },
{ value: 90, color: 'red' },
],
field: field.config!,
display: field.display!,
height: 300,
width: 300,
value: {
text: '25',
numeric: 25,
},
theme: getTheme(),
value: field.display(25),
theme,
orientation: VizOrientation.Horizontal,
};
@@ -59,11 +68,13 @@ function getValue(value: number, title?: string): DisplayValue {
describe('BarGauge', () => {
describe('Get value color', () => {
it('should get the threshold color if value is same as a threshold', () => {
const props = getProps({ value: getValue(70) });
const props = getProps();
props.value = props.display(70);
expect(getValueColor(props)).toEqual(orange);
});
it('should get the base threshold', () => {
const props = getProps({ value: getValue(-10) });
const props = getProps();
props.value = props.display(-10);
expect(getValueColor(props)).toEqual(green);
});
});
@@ -1,14 +1,17 @@
// Library
import React, { PureComponent, CSSProperties, ReactNode } from 'react';
import tinycolor from 'tinycolor2';
import * as d3 from 'd3-scale-chromatic';
import {
Threshold,
TimeSeriesValue,
getActiveThreshold,
DisplayValue,
formattedValueToString,
FormattedValue,
DisplayValueAlignmentFactors,
ThresholdsMode,
DisplayProcessor,
FieldConfig,
FieldColorMode,
} from '@grafana/data';
// Compontents
@@ -33,10 +36,9 @@ const VALUE_LEFT_PADDING = 10;
export interface Props extends Themeable {
height: number;
width: number;
thresholds: Threshold[];
field: FieldConfig;
display: DisplayProcessor;
value: DisplayValue;
maxValue: number;
minValue: number;
orientation: VizOrientation;
itemSpacing?: number;
lcdCellWidth?: number;
@@ -55,8 +57,6 @@ export enum BarGaugeDisplayMode {
export class BarGauge extends PureComponent<Props> {
static defaultProps: Partial<Props> = {
maxValue: 100,
minValue: 0,
lcdCellWidth: 12,
value: {
text: '100',
@@ -64,7 +64,14 @@ export class BarGauge extends PureComponent<Props> {
},
displayMode: BarGaugeDisplayMode.Gradient,
orientation: VizOrientation.Horizontal,
thresholds: [],
field: {
min: 0,
max: 100,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [],
},
},
itemSpacing: 10,
showUnfilled: true,
};
@@ -116,7 +123,7 @@ export class BarGauge extends PureComponent<Props> {
}
getCellColor(positionValue: TimeSeriesValue): CellColors {
const { thresholds, theme, value } = this.props;
const { value, display } = this.props;
if (positionValue === null) {
return {
background: 'gray',
@@ -124,10 +131,8 @@ export class BarGauge extends PureComponent<Props> {
};
}
const activeThreshold = getActiveThreshold(positionValue, thresholds);
if (activeThreshold !== null) {
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
const color = display(positionValue).color;
if (color) {
// if we are past real value the cell is not "on"
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
return {
@@ -160,7 +165,7 @@ export class BarGauge extends PureComponent<Props> {
}
renderRetroBars(): ReactNode {
const { maxValue, minValue, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
const {
valueHeight,
valueWidth,
@@ -169,6 +174,8 @@ export class BarGauge extends PureComponent<Props> {
wrapperWidth,
wrapperHeight,
} = calculateBarAndValueDimensions(this.props);
const minValue = field.min!;
const maxValue = field.max!;
const isVert = isVertical(orientation);
const valueRange = maxValue - minValue;
@@ -402,10 +409,10 @@ export function getValuePercent(value: number, minValue: number, maxValue: numbe
* Only exported to for unit test
*/
export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles {
const { displayMode, maxValue, minValue, value, alignmentFactors, orientation, theme } = props;
const { displayMode, field, value, alignmentFactors, orientation, theme } = props;
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
const valuePercent = getValuePercent(value.numeric, minValue, maxValue);
const valuePercent = getValuePercent(value.numeric, field.min!, field.max!);
const valueColor = getValueColor(props);
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
@@ -495,26 +502,56 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
* Only exported to for unit test
*/
export function getBarGradient(props: Props, maxSize: number): string {
const { minValue, maxValue, thresholds, value, orientation } = props;
const { field, value, orientation } = props;
const cssDirection = isVertical(orientation) ? '0deg' : '90deg';
const minValue = field.min!;
const maxValue = field.max!;
let gradient = '';
let lastpos = 0;
for (let i = 0; i < thresholds.length; i++) {
const threshold = thresholds[i];
const color = getColorFromHexRgbOrName(threshold.color);
const valuePercent = getValuePercent(threshold.value, minValue, maxValue);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
if (gradient === '') {
if (field.color && field.color.mode === FieldColorMode.Scheme) {
const schemeSet = (d3 as any)[`scheme${field.color.schemeName}`] as any[];
if (!schemeSet) {
// Error: unknown scheme
const color = '#F00';
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
} else if (value.numeric < threshold.value) {
break;
} else {
lastpos = pos;
gradient += ` ${offset}px, ${color}`;
gradient += ` ${maxSize}px, ${color}`;
return gradient + ')';
}
// Get the scheme with as many steps as possible
const scheme = schemeSet[schemeSet.length - 1] as string[];
for (let i = 0; i < scheme.length; i++) {
const color = scheme[i];
const valuePercent = i / (scheme.length - 1);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
if (gradient === '') {
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
} else {
lastpos = pos;
gradient += ` ${offset}px, ${color}`;
}
}
} else {
const thresholds = field.thresholds!;
for (let i = 0; i < thresholds.steps.length; i++) {
const threshold = thresholds.steps[i];
const color = getColorFromHexRgbOrName(threshold.color);
const valuePercent = getValuePercent(threshold.value, minValue, maxValue);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
if (gradient === '') {
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
} else if (value.numeric < threshold.value) {
break;
} else {
lastpos = pos;
gradient += ` ${offset}px, ${color}`;
}
}
}
@@ -525,14 +562,10 @@ export function getBarGradient(props: Props, maxSize: number): string {
* Only exported to for unit test
*/
export function getValueColor(props: Props): string {
const { thresholds, theme, value } = props;
const activeThreshold = getActiveThreshold(value.numeric, thresholds);
if (activeThreshold !== null) {
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
const { theme, value } = props;
if (value.color) {
return value.color;
}
return getColorFromHexRgbOrName('gray', theme.type);
}
@@ -40,8 +40,15 @@ exports[`BarGauge Render with basic options should render 1`] = `
}
value={
Object {
"color": "#73BF69",
"numeric": 25,
"prefix": undefined,
"suffix": undefined,
"text": "25",
"threshold": Object {
"color": "green",
"value": -Infinity,
},
}
}
/>
@@ -3,20 +3,29 @@ import { shallow } from 'enzyme';
import { Gauge, Props } from './Gauge';
import { getTheme } from '../../themes';
import { ThresholdsMode, FieldConfig } from '@grafana/data';
jest.mock('jquery', () => ({
plot: jest.fn(),
}));
const setup = (propOverrides?: object) => {
const setup = (propOverrides?: FieldConfig) => {
const field: FieldConfig = {
min: 0,
max: 100,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [{ value: -Infinity, color: '#7EB26D' }],
},
};
Object.assign(field, propOverrides);
const props: Props = {
maxValue: 100,
minValue: 0,
showThresholdMarkers: true,
showThresholdLabels: false,
thresholds: [{ value: -Infinity, color: '#7EB26D' }],
height: 300,
field,
width: 300,
height: 300,
value: {
text: '25',
numeric: 25,
@@ -24,8 +33,6 @@ const setup = (propOverrides?: object) => {
theme: getTheme(),
};
Object.assign(props, propOverrides);
const wrapper = shallow(<Gauge {...props} />);
const instance = wrapper.instance() as Gauge;
@@ -37,7 +44,9 @@ const setup = (propOverrides?: object) => {
describe('Get thresholds formatted', () => {
it('should return first thresholds color for min and max', () => {
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
const { instance } = setup({
thresholds: { mode: ThresholdsMode.Absolute, steps: [{ value: -Infinity, color: '#7EB26D' }] },
});
expect(instance.getFormattedThresholds()).toEqual([
{ value: 0, color: '#7EB26D' },
@@ -47,11 +56,14 @@ describe('Get thresholds formatted', () => {
it('should get the correct formatted values when thresholds are added', () => {
const { instance } = setup({
thresholds: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
},
});
expect(instance.getFormattedThresholds()).toEqual([
@@ -1,14 +1,20 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { Threshold, DisplayValue, getColorFromHexRgbOrName, formattedValueToString } from '@grafana/data';
import {
DisplayValue,
getColorFromHexRgbOrName,
formattedValueToString,
FieldConfig,
ThresholdsMode,
getActiveThreshold,
Threshold,
} from '@grafana/data';
import { Themeable } from '../../types';
import { selectThemeVariant } from '../../themes';
export interface Props extends Themeable {
height: number;
maxValue: number;
minValue: number;
thresholds: Threshold[];
field: FieldConfig;
showThresholdMarkers: boolean;
showThresholdLabels: boolean;
width: number;
@@ -23,11 +29,19 @@ export class Gauge extends PureComponent<Props> {
canvasElement: any;
static defaultProps: Partial<Props> = {
maxValue: 100,
minValue: 0,
showThresholdMarkers: true,
showThresholdLabels: false,
thresholds: [],
field: {
min: 0,
max: 100,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' },
],
},
},
};
componentDidMount() {
@@ -38,22 +52,38 @@ export class Gauge extends PureComponent<Props> {
this.draw();
}
getFormattedThresholds() {
const { maxValue, minValue, thresholds, theme } = this.props;
getFormattedThresholds(): Threshold[] {
const { field, theme } = this.props;
const isPercent = field.thresholds?.mode === ThresholdsMode.Percentage;
const steps = field.thresholds!.steps;
let min = field.min!;
let max = field.max!;
if (isPercent) {
min = 0;
max = 100;
}
const lastThreshold = thresholds[thresholds.length - 1];
return [
...thresholds.map((threshold, index) => {
if (index === 0) {
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
const first = getActiveThreshold(min, steps);
const last = getActiveThreshold(max, steps);
const formatted: Threshold[] = [];
formatted.push({ value: min, color: getColorFromHexRgbOrName(first.color, theme.type) });
let skip = true;
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
if (skip) {
if (first === step) {
skip = false;
}
const previousThreshold = thresholds[index - 1];
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
}),
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
];
continue;
}
const prev = steps[i - 1];
formatted.push({ value: step.value, color: getColorFromHexRgbOrName(prev!.color, theme.type) });
if (step === last) {
break;
}
}
formatted.push({ value: max, color: getColorFromHexRgbOrName(last.color, theme.type) });
return formatted;
}
getFontScale(length: number): number {
@@ -64,7 +94,7 @@ export class Gauge extends PureComponent<Props> {
}
draw() {
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
const { field, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
const autoProps = calculateGaugeAutoProps(width, height, value.title);
const dimension = Math.min(width, autoProps.gaugeHeight);
@@ -85,12 +115,25 @@ export class Gauge extends PureComponent<Props> {
const thresholdLabelFontSize = fontSize / 2.5;
let min = field.min!;
let max = field.max!;
let numeric = value.numeric;
if (field.thresholds?.mode === ThresholdsMode.Percentage) {
min = 0;
max = 100;
if (value.percent === undefined) {
numeric = ((numeric - min) / (max - min)) * 100;
} else {
numeric = value.percent! * 100;
}
}
const options: any = {
series: {
gauges: {
gauge: {
min: minValue,
max: maxValue,
min,
max,
background: { color: backgroundColor },
border: { color: null },
shadow: { show: false },
@@ -123,7 +166,7 @@ export class Gauge extends PureComponent<Props> {
};
const plotSeries = {
data: [[0, value.numeric]],
data: [[0, numeric]],
label: value.title,
};
@@ -1,7 +1,7 @@
import React from 'react';
import { Graph } from './Graph';
import Chart from '../Chart';
import { dateTime, ArrayVector, FieldType, GraphSeriesXY } from '@grafana/data';
import { dateTime, ArrayVector, FieldType, GraphSeriesXY, FieldColorMode } from '@grafana/data';
import { select } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { TooltipContentProps } from '../Chart/Tooltip';
@@ -47,7 +47,12 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'a-series',
values: new ArrayVector([10, 20, 10]),
config: { color: 'red' },
config: {
color: {
mode: FieldColorMode.Fixed,
fixedColor: 'red',
},
},
},
timeStep: 3600000,
yAxis: {
@@ -76,7 +81,12 @@ const series: GraphSeriesXY[] = [
name:
"B-series with an ultra wide label that is probably going go make the tooltip overflow window. This situation happens, so let's better make sure it behaves nicely :)",
values: new ArrayVector([20, 30, 40]),
config: { color: 'blue' },
config: {
color: {
mode: FieldColorMode.Fixed,
fixedColor: 'blue',
},
},
},
timeStep: 3600000,
yAxis: {
@@ -2,7 +2,7 @@ import React from 'react';
import { mount } from 'enzyme';
import Graph from './Graph';
import Chart from '../Chart';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime } from '@grafana/data';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorMode } from '@grafana/data';
const series: GraphSeriesXY[] = [
{
@@ -25,7 +25,7 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'a-series',
values: new ArrayVector([10, 20, 10]),
config: { color: 'red' },
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'red' } },
},
timeStep: 3600000,
yAxis: {
@@ -52,7 +52,7 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'b-series',
values: new ArrayVector([20, 30, 40]),
config: { color: 'blue' },
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'blue' } },
},
timeStep: 3600000,
yAxis: {
@@ -1,5 +1,10 @@
import React from 'react';
import { getValueFromDimension, getColumnFromDimension, formattedValueToString } from '@grafana/data';
import {
getValueFromDimension,
getColumnFromDimension,
formattedValueToString,
getDisplayProcessor,
} from '@grafana/data';
import { SeriesTable } from './SeriesTable';
import { GraphTooltipContentProps } from './types';
@@ -19,11 +24,18 @@ export const SingleModeGraphTooltip: React.FC<GraphTooltipContentProps> = ({ dim
const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]);
const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]);
const processedValue = valueField.display ? formattedValueToString(valueField.display(value)) : value;
const display = valueField.display ?? getDisplayProcessor({ field: valueField });
const disp = display(value);
return (
<SeriesTable
series={[{ color: valueField.config.color, label: valueField.name, value: processedValue }]}
series={[
{
color: disp.color,
label: valueField.name,
value: formattedValueToString(disp),
},
]}
timestamp={processedTime}
/>
);
@@ -6,7 +6,7 @@ import { withHorizontallyCenteredStory } from '../../utils/storybook/withCentere
import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend';
import { LegendPlacement, LegendDisplayMode } from '../Legend/Legend';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime } from '@grafana/data';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorMode } from '@grafana/data';
const GraphWithLegendStories = storiesOf('Visualizations/Graph/GraphWithLegend', module);
GraphWithLegendStories.addDecorator(withHorizontallyCenteredStory);
@@ -31,7 +31,12 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'a-series',
values: new ArrayVector([10, 20, 10]),
config: { color: 'red' },
config: {
color: {
mode: FieldColorMode.Fixed,
fixedColor: 'red',
},
},
},
timeStep: 3600000,
yAxis: {
@@ -58,7 +63,12 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'b-series',
values: new ArrayVector([20, 30, 40]),
config: { color: 'blue' },
config: {
color: {
mode: FieldColorMode.Fixed,
fixedColor: 'blue',
},
},
},
timeStep: 3600000,
yAxis: {
@@ -1,8 +1,17 @@
import { GraphSeriesValue, toDataFrame, FieldType, FieldCache } from '@grafana/data';
import {
GraphSeriesValue,
toDataFrame,
FieldType,
FieldCache,
FieldColorMode,
getColorFromHexRgbOrName,
GrafanaThemeType,
Field,
} from '@grafana/data';
import { getMultiSeriesGraphHoverInfo, findHoverIndexFromData } from './utils';
const mockResult = (
value: GraphSeriesValue,
value: string,
datapointIndex: number,
seriesIndex: number,
color?: string,
@@ -21,23 +30,42 @@ const mockResult = (
const aSeries = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'value', type: FieldType.number, values: [10, 20, 10], config: { color: 'red' } },
{
name: 'value',
type: FieldType.number,
values: [10, 20, 10],
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'red' } },
},
],
});
const bSeries = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'value', type: FieldType.number, values: [30, 60, 30], config: { color: 'blue' } },
{
name: 'value',
type: FieldType.number,
values: [30, 60, 30],
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'blue' } },
},
],
});
// C-series has the same x-axis range as A and B but is missing the middle point
const cSeries = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [100, 300] },
{ name: 'value', type: FieldType.number, values: [30, 30], config: { color: 'yellow' } },
{
name: 'value',
type: FieldType.number,
values: [30, 30],
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'yellow' } },
},
],
});
function getFixedThemedColor(field: Field): string {
return getColorFromHexRgbOrName(field.config.color!.fixedColor!, GrafanaThemeType.Dark);
}
describe('Graph utils', () => {
describe('getMultiSeriesGraphHoverInfo', () => {
describe('when series datapoints are x-axis aligned', () => {
@@ -51,8 +79,12 @@ describe('Graph utils', () => {
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 0);
expect(result.time).toBe(100);
expect(result.results[0]).toEqual(mockResult(10, 0, 0, aValueField!.config.color, aValueField!.name, 100));
expect(result.results[1]).toEqual(mockResult(30, 0, 1, bValueField!.config.color, bValueField!.name, 100));
expect(result.results[0]).toEqual(
mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, 100)
);
expect(result.results[1]).toEqual(
mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, 100)
);
});
describe('returns the closest datapoints before the hover position', () => {
@@ -67,8 +99,12 @@ describe('Graph utils', () => {
// hovering right before middle point
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 199);
expect(result.time).toBe(100);
expect(result.results[0]).toEqual(mockResult(10, 0, 0, aValueField!.config.color, aValueField!.name, 100));
expect(result.results[1]).toEqual(mockResult(30, 0, 1, bValueField!.config.color, bValueField!.name, 100));
expect(result.results[0]).toEqual(
mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, 100)
);
expect(result.results[1]).toEqual(
mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, 100)
);
});
it('when hovering right after a datapoint', () => {
@@ -82,8 +118,12 @@ describe('Graph utils', () => {
// hovering right after middle point
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 201);
expect(result.time).toBe(200);
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
expect(result.results[1]).toEqual(mockResult(60, 1, 1, bValueField!.config.color, bValueField!.name, 200));
expect(result.results[0]).toEqual(
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
);
expect(result.results[1]).toEqual(
mockResult('60', 1, 1, getFixedThemedColor(bValueField!), bValueField!.name, 200)
);
});
});
});
@@ -106,9 +146,13 @@ describe('Graph utils', () => {
// we expect a time of the hovered point
expect(result.time).toBe(200);
// we expect middle point from aSeries (the one we are hovering over)
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
expect(result.results[0]).toEqual(
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
);
// we expect closest point before hovered point from cSeries (1st point)
expect(result.results[1]).toEqual(mockResult(30, 0, 1, cValueField!.config.color, cValueField!.name, 100));
expect(result.results[1]).toEqual(
mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, 100)
);
});
it('hovering right after over the middle point', () => {
@@ -125,9 +169,13 @@ describe('Graph utils', () => {
// we expect the time of the closest point before hover
expect(result.time).toBe(200);
// we expect the closest datapoint before hover from aSeries
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
expect(result.results[0]).toEqual(
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
);
// we expect the closest datapoint before hover from cSeries (1st point)
expect(result.results[1]).toEqual(mockResult(30, 0, 1, cValueField!.config.color, cValueField!.name, 100));
expect(result.results[1]).toEqual(
mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, 100)
);
});
});
});
@@ -1,4 +1,4 @@
import { GraphSeriesValue, Field, formattedValueToString } from '@grafana/data';
import { GraphSeriesValue, Field, formattedValueToString, getDisplayProcessor } from '@grafana/data';
/**
* Returns index of the closest datapoint BEFORE hover position
@@ -53,14 +53,14 @@ export const getMultiSeriesGraphHoverInfo = (
results: MultiSeriesHoverInfo[];
time?: GraphSeriesValue;
} => {
let value, i, series, hoverIndex, hoverDistance, pointTime;
let i, field, hoverIndex, hoverDistance, pointTime;
const results: MultiSeriesHoverInfo[] = [];
let minDistance, minTime;
for (i = 0; i < yAxisDimensions.length; i++) {
series = yAxisDimensions[i];
field = yAxisDimensions[i];
const time = xAxisDimensions[i];
hoverIndex = findHoverIndexFromData(time, xAxisPosition);
hoverDistance = xAxisPosition - time.values.get(hoverIndex);
@@ -75,14 +75,15 @@ export const getMultiSeriesGraphHoverInfo = (
minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime;
}
value = series.values.get(hoverIndex);
const display = field.display ?? getDisplayProcessor({ field });
const disp = display(field.values.get(hoverIndex));
results.push({
value: series.display ? formattedValueToString(series.display(value)) : value,
value: formattedValueToString(disp),
datapointIndex: hoverIndex,
seriesIndex: i,
color: series.config.color,
label: series.name,
color: disp.color,
label: field.name,
time: time.display ? formattedValueToString(time.display(pointTime)) : pointTime,
});
}
@@ -37,6 +37,36 @@ describe('sharedSingleStatMigrationHandler', () => {
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
});
it('move thresholds to scale', () => {
const panel = {
options: {
fieldOptions: {
defaults: {
thresholds: [
{
color: 'green',
index: 0,
value: null,
},
{
color: 'orange',
index: 1,
value: 40,
},
{
color: 'red',
index: 2,
value: 80,
},
],
},
},
},
};
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
});
it('Remove unused `overrides` option', () => {
const panel = {
options: {
@@ -13,6 +13,10 @@ import {
PanelModel,
FieldDisplayOptions,
ConfigOverrideRule,
ThresholdsMode,
ThresholdsConfig,
validateFieldConfig,
FieldColorMode,
} from '@grafana/data';
export interface SingleStatBaseOptions {
@@ -70,7 +74,10 @@ export function sharedSingleStatPanelChangedHandler(
thresholds.push({ value: -Infinity, color });
}
}
defaults.thresholds = thresholds;
defaults.thresholds = {
mode: ThresholdsMode.Absolute,
steps: thresholds,
};
}
// Convert value mappings
@@ -112,8 +119,10 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
}
if (previousVersion < 6.6) {
const { fieldOptions } = options;
// discard the old `override` options and enter an empty array
if (options.fieldOptions && options.fieldOptions.override) {
if (fieldOptions && fieldOptions.override) {
const { override, ...rest } = options.fieldOptions;
options = {
...options,
@@ -123,6 +132,34 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
},
};
}
// Move thresholds to steps
let thresholds = fieldOptions?.defaults?.thresholds;
if (thresholds) {
delete fieldOptions.defaults.thresholds;
} else {
thresholds = fieldOptions?.thresholds;
delete fieldOptions.thresholds;
}
if (thresholds) {
fieldOptions.defaults.thresholds = {
mode: ThresholdsMode.Absolute,
steps: thresholds,
};
}
// Migrate color from simple string to a mode
const { defaults } = fieldOptions;
if (defaults.color) {
const old = defaults.color;
defaults.color = {
mode: FieldColorMode.Fixed,
fixedColor: old,
};
}
validateFieldConfig(defaults);
}
return options as SingleStatBaseOptions;
@@ -135,7 +172,15 @@ export function moveThresholdsAndMappingsToField(old: any) {
return old;
}
const { mappings, thresholds, ...rest } = old.fieldOptions;
const { mappings, ...rest } = old.fieldOptions;
let thresholds: ThresholdsConfig | undefined = undefined;
if (old.thresholds) {
thresholds = {
mode: ThresholdsMode.Absolute,
steps: migrateOldThresholds(old.thresholds)!,
};
}
return {
...old,
@@ -144,7 +189,7 @@ export function moveThresholdsAndMappingsToField(old: any) {
defaults: {
...fieldOptions.defaults,
mappings,
thresholds: migrateOldThresholds(thresholds),
thresholds,
},
},
};
@@ -24,6 +24,9 @@ Object {
"last",
],
"defaults": Object {
"color": Object {
"mode": "thresholds",
},
"decimals": 5,
"mappings": Array [
Object {
@@ -34,22 +37,39 @@ Object {
],
"max": 100,
"min": 10,
"thresholds": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "orange",
"value": 40,
},
Object {
"color": "red",
"value": 80,
},
],
"thresholds": Object {
"mode": "absolute",
"steps": Array [
Object {
"color": "green",
"index": 0,
"value": -Infinity,
},
Object {
"color": "orange",
"index": 1,
"value": 40,
},
Object {
"color": "red",
"index": 2,
"value": 80,
},
],
},
"unit": "watt",
},
},
}
`;
exports[`sharedSingleStatMigrationHandler move thresholds to scale 1`] = `
Object {
"fieldOptions": Object {
"defaults": Object {
"mappings": undefined,
"thresholds": undefined,
},
},
}
`;
@@ -1,18 +1,21 @@
import React, { FC } from 'react';
import { ReactTableCellProps, TableCellDisplayMode } from './types';
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
import { VizOrientation } from '@grafana/data';
import { ThresholdsConfig, ThresholdsMode, VizOrientation } from '@grafana/data';
const defaultThresholds = [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
];
const defaultScale: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
steps: [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
],
};
export const BarGaugeCell: FC<ReactTableCellProps> = props => {
const { column, tableStyles, cell } = props;
@@ -22,6 +25,14 @@ export const BarGaugeCell: FC<ReactTableCellProps> = props => {
return null;
}
let { config } = field;
if (!config.thresholds) {
config = {
...config,
thresholds: defaultScale,
};
}
const displayValue = field.display(cell.value);
let barGaugeMode = BarGaugeDisplayMode.Gradient;
@@ -34,10 +45,8 @@ export const BarGaugeCell: FC<ReactTableCellProps> = props => {
<BarGauge
width={column.width - tableStyles.cellPadding * 2}
height={tableStyles.cellHeightInner}
thresholds={field.config.thresholds || defaultThresholds}
field={config}
value={displayValue}
maxValue={field.config.max || 100}
minValue={field.config.min || 0}
orientation={VizOrientation.Horizontal}
theme={tableStyles.theme}
itemSpacing={1}
@@ -3,17 +3,24 @@ import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ThresholdsEditor } from './ThresholdsEditor';
import { getTheme } from '../../themes';
import { ThresholdsMode, ThresholdsConfig } from '@grafana/data';
const ThresholdsEditorStories = storiesOf('UI/ThresholdsEditor', module);
const thresholds = [
{ index: 0, value: -Infinity, color: 'green' },
{ index: 1, value: 50, color: 'red' },
];
const thresholds: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 50, color: 'red' },
],
};
ThresholdsEditorStories.add('default', () => {
return <ThresholdsEditor thresholds={[]} onChange={action('Thresholds changed')} />;
return (
<ThresholdsEditor theme={getTheme()} thresholds={{} as ThresholdsConfig} onChange={action('Thresholds changed')} />
);
});
ThresholdsEditorStories.add('with thresholds', () => {
return <ThresholdsEditor thresholds={thresholds} onChange={action('Thresholds changed')} />;
return <ThresholdsEditor theme={getTheme()} thresholds={thresholds} onChange={action('Thresholds changed')} />;
});
@@ -1,14 +1,15 @@
import React, { ChangeEvent } from 'react';
import { mount } from 'enzyme';
import { GrafanaThemeType } from '@grafana/data';
import { GrafanaThemeType, GrafanaTheme, ThresholdsMode } from '@grafana/data';
import { ThresholdsEditor, Props, thresholdsWithoutKey } from './ThresholdsEditor';
import { colors } from '../../utils';
import { mockThemeContext } from '../../themes/ThemeContext';
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
theme: { type: GrafanaThemeType.Dark, isDark: true, isLight: false } as GrafanaTheme,
onChange: jest.fn(),
thresholds: [],
thresholds: { mode: ThresholdsMode.Absolute, steps: [] },
};
Object.assign(props, propOverrides);
@@ -23,7 +24,7 @@ const setup = (propOverrides?: Partial<Props>) => {
};
function getCurrentThresholds(editor: ThresholdsEditor) {
return thresholdsWithoutKey(editor.state.thresholds);
return thresholdsWithoutKey(editor.props.thresholds, editor.state.steps);
}
describe('Render', () => {
@@ -38,14 +39,14 @@ describe('Render', () => {
it('should render with base threshold', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.thresholds')).toMatchSnapshot();
});
});
describe('Initialization', () => {
it('should add a base threshold if missing', () => {
const { instance } = setup();
expect(getCurrentThresholds(instance)).toEqual([{ value: -Infinity, color: 'green' }]);
expect(getCurrentThresholds(instance).steps).toEqual([{ value: -Infinity, color: 'green' }]);
});
});
@@ -53,9 +54,9 @@ describe('Add threshold', () => {
it('should add threshold', () => {
const { instance } = setup();
instance.onAddThresholdAfter(instance.state.thresholds[0]);
instance.onAddThresholdAfter(instance.state.steps[0]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: 'green' }, // 0
{ value: 50, color: colors[1] }, // 1
]);
@@ -63,15 +64,18 @@ describe('Add threshold', () => {
it('should add another threshold above a first', () => {
const { instance } = setup({
thresholds: [
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
],
},
});
instance.onAddThresholdAfter(instance.state.thresholds[1]);
instance.onAddThresholdAfter(instance.state.steps[1]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
{ value: 75, color: colors[3] }, // 2
@@ -80,16 +84,19 @@ describe('Add threshold', () => {
it('should add another threshold between first and second index', () => {
const { instance } = setup({
thresholds: [
{ value: -Infinity, color: colors[0] },
{ value: 50, color: colors[2] },
{ value: 75, color: colors[3] },
],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: colors[0] },
{ value: 50, color: colors[2] },
{ value: 75, color: colors[3] },
],
},
});
instance.onAddThresholdAfter(instance.state.thresholds[1]);
instance.onAddThresholdAfter(instance.state.steps[1]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: colors[0] },
{ value: 50, color: colors[2] },
{ value: 62.5, color: colors[4] },
@@ -100,29 +107,35 @@ describe('Add threshold', () => {
describe('Remove threshold', () => {
it('should not remove threshold at index 0', () => {
const thresholds = [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
};
const { instance } = setup({ thresholds });
instance.onRemoveThreshold(instance.state.thresholds[0]);
instance.onRemoveThreshold(instance.state.steps[0]);
expect(getCurrentThresholds(instance)).toEqual(thresholds);
});
it('should remove threshold', () => {
const thresholds = [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
};
const { instance } = setup({ thresholds });
instance.onRemoveThreshold(instance.state.thresholds[1]);
instance.onRemoveThreshold(instance.state.steps[1]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 75, color: '#6ED0E0' },
]);
@@ -131,37 +144,43 @@ describe('Remove threshold', () => {
describe('change threshold value', () => {
it('should not change threshold at index 0', () => {
const thresholds = [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
};
const { instance } = setup({ thresholds });
const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
instance.onChangeThresholdValue(mockEvent, instance.state.thresholds[0]);
instance.onChangeThresholdValue(mockEvent, instance.state.steps[0]);
expect(getCurrentThresholds(instance)).toEqual(thresholds);
});
it('should update value', () => {
const { instance } = setup();
const thresholds = [
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 50, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 50, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
],
};
instance.state = {
thresholds,
steps: thresholds.steps,
};
const mockEvent = ({ target: { value: '78' } } as any) as ChangeEvent<HTMLInputElement>;
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
instance.onChangeThresholdValue(mockEvent, thresholds.steps[1]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 78, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
@@ -172,19 +191,22 @@ describe('change threshold value', () => {
describe('on blur threshold value', () => {
it('should resort rows and update indexes', () => {
const { instance } = setup();
const thresholds = [
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 78, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 78, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
],
};
instance.setState({
thresholds,
steps: thresholds.steps,
});
instance.onBlur();
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 75, color: '#6ED0E0' },
{ value: 78, color: '#EAB839' },
@@ -1,19 +1,31 @@
import React, { PureComponent, ChangeEvent } from 'react';
import { Threshold, sortThresholds } from '@grafana/data';
import { Threshold, sortThresholds, ThresholdsConfig, ThresholdsMode, SelectableValue } from '@grafana/data';
import { colors } from '../../utils';
import { ThemeContext } from '../../themes';
import { getColorFromHexRgbOrName } from '@grafana/data';
import { Input } from '../Input/Input';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { Themeable } from '../../types';
import { css } from 'emotion';
import Select from '../Select/Select';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
export interface Props {
thresholds?: Threshold[];
onChange: (thresholds: Threshold[]) => void;
const modes: Array<SelectableValue<ThresholdsMode>> = [
{ value: ThresholdsMode.Absolute, label: 'Absolute', description: 'Pick thresholds based on the absolute values' },
{
value: ThresholdsMode.Percentage,
label: 'Percentage',
description: 'Pick threshold based on the percent between min/max',
},
];
export interface Props extends Themeable {
showAlphaUI?: boolean;
thresholds: ThresholdsConfig;
onChange: (thresholds: ThresholdsConfig) => void;
}
interface State {
thresholds: ThresholdWithKey[];
steps: ThresholdWithKey[];
}
interface ThresholdWithKey extends Threshold {
@@ -22,12 +34,12 @@ interface ThresholdWithKey extends Threshold {
let counter = 100;
function toThresholdsWithKey(thresholds?: Threshold[]): ThresholdWithKey[] {
if (!thresholds || thresholds.length === 0) {
thresholds = [{ value: -Infinity, color: 'green' }];
function toThresholdsWithKey(steps?: Threshold[]): ThresholdWithKey[] {
if (!steps || steps.length === 0) {
steps = [{ value: -Infinity, color: 'green' }];
}
return thresholds.map(t => {
return steps.map(t => {
return {
color: t.color,
value: t.value === null ? -Infinity : t.value,
@@ -40,21 +52,21 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const thresholds = toThresholdsWithKey(props.thresholds);
thresholds[0].value = -Infinity;
const steps = toThresholdsWithKey(props.thresholds!.steps);
steps[0].value = -Infinity;
this.state = { thresholds };
this.state = { steps };
}
onAddThresholdAfter = (threshold: ThresholdWithKey) => {
const { thresholds } = this.state;
const { steps } = this.state;
const maxValue = 100;
const minValue = 0;
let prev: ThresholdWithKey | undefined = undefined;
let next: ThresholdWithKey | undefined = undefined;
for (const t of thresholds) {
for (const t of steps) {
if (prev && prev.key === threshold.key) {
next = t;
break;
@@ -65,35 +77,35 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
const prevValue = prev && isFinite(prev.value) ? prev.value : minValue;
const nextValue = next && isFinite(next.value) ? next.value : maxValue;
const color = colors.filter(c => !thresholds.some(t => t.color === c))[1];
const color = colors.filter(c => !steps.some(t => t.color === c))[1];
const add = {
value: prevValue + (nextValue - prevValue) / 2.0,
color: color,
key: counter++,
};
const newThresholds = [...thresholds, add];
const newThresholds = [...steps, add];
sortThresholds(newThresholds);
this.setState(
{
thresholds: newThresholds,
steps: newThresholds,
},
() => this.onChange()
);
};
onRemoveThreshold = (threshold: ThresholdWithKey) => {
const { thresholds } = this.state;
if (!thresholds.length) {
const { steps } = this.state;
if (!steps.length) {
return;
}
// Don't remove index 0
if (threshold.key === thresholds[0].key) {
if (threshold.key === steps[0].key) {
return;
}
this.setState(
{
thresholds: thresholds.filter(t => t.key !== threshold.key),
steps: steps.filter(t => t.key !== threshold.key),
},
() => this.onChange()
);
@@ -104,22 +116,22 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
const parsedValue = parseFloat(cleanValue);
const value = isNaN(parsedValue) ? '' : parsedValue;
const thresholds = this.state.thresholds.map(t => {
const steps = this.state.steps.map(t => {
if (t.key === threshold.key) {
t = { ...t, value: value as number };
}
return t;
});
if (thresholds.length) {
thresholds[0].value = -Infinity;
if (steps.length) {
steps[0].value = -Infinity;
}
this.setState({ thresholds });
this.setState({ steps });
};
onChangeThresholdColor = (threshold: ThresholdWithKey, color: string) => {
const { thresholds } = this.state;
const { steps } = this.state;
const newThresholds = thresholds.map(t => {
const newThresholds = steps.map(t => {
if (t.key === threshold.key) {
t = { ...t, color: color };
}
@@ -129,29 +141,38 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
this.setState(
{
thresholds: newThresholds,
steps: newThresholds,
},
() => this.onChange()
);
};
onBlur = () => {
const thresholds = [...this.state.thresholds];
sortThresholds(thresholds);
const steps = [...this.state.steps];
sortThresholds(steps);
this.setState(
{
thresholds,
steps,
},
() => this.onChange()
);
};
onChange = () => {
const { thresholds } = this.state;
this.props.onChange(thresholdsWithoutKey(thresholds));
this.props.onChange(thresholdsWithoutKey(this.props.thresholds, this.state.steps));
};
onModeChanged = (item: SelectableValue<ThresholdsMode>) => {
if (item.value) {
this.props.onChange({
...this.props.thresholds,
mode: item.value,
});
}
};
renderInput = (threshold: ThresholdWithKey) => {
const isPercent = this.props.thresholds.mode === ThresholdsMode.Percentage;
return (
<div className="thresholds-row-input-inner">
<span className="thresholds-row-input-inner-arrow" />
@@ -181,6 +202,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
onBlur={this.onBlur}
/>
</div>
{isPercent && (
<div className={css(`margin-left:-20px; margin-top:5px;`)}>
<i className="fa fa-percent" />
</div>
)}
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
<i className="fa fa-times" />
</div>
@@ -191,42 +217,50 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
};
render() {
const { thresholds } = this.state;
const { steps } = this.state;
const { theme } = this.props;
const t = this.props.thresholds;
return (
<ThemeContext.Consumer>
{theme => {
return (
<PanelOptionsGroup title="Thresholds">
<div className="thresholds">
{thresholds
.slice(0)
.reverse()
.map(threshold => {
return (
<div className="thresholds-row" key={`${threshold.key}`}>
<div className="thresholds-row-add-button" onClick={() => this.onAddThresholdAfter(threshold)}>
<i className="fa fa-plus" />
</div>
<div
className="thresholds-row-color-indicator"
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
);
})}
</div>
</PanelOptionsGroup>
);
}}
</ThemeContext.Consumer>
<PanelOptionsGroup title="Thresholds">
<>
<div className="thresholds">
{steps
.slice(0)
.reverse()
.map(threshold => {
return (
<div className="thresholds-row" key={`${threshold.key}`}>
<div className="thresholds-row-add-button" onClick={() => this.onAddThresholdAfter(threshold)}>
<i className="fa fa-plus" />
</div>
<div
className="thresholds-row-color-indicator"
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
);
})}
</div>
{this.props.showAlphaUI && (
<div>
<Select options={modes} value={modes.filter(m => m.value === t.mode)} onChange={this.onModeChanged} />
</div>
)}
</>
</PanelOptionsGroup>
);
}
}
export function thresholdsWithoutKey(thresholds: ThresholdWithKey[]): Threshold[] {
return thresholds.map(t => {
const { key, ...rest } = t;
return rest; // everything except key
});
export function thresholdsWithoutKey(thresholds: ThresholdsConfig, steps: ThresholdWithKey[]): ThresholdsConfig {
const mode = thresholds.mode ?? ThresholdsMode.Absolute;
return {
mode,
steps: steps.map(t => {
const { key, ...rest } = t;
return rest; // everything except key
}),
};
}
@@ -1,182 +1,156 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render with base threshold 1`] = `
<ThresholdsEditor
onChange={[MockFunction]}
thresholds={Array []}
<div
className="thresholds"
>
<Component
title="Thresholds"
<div
className="thresholds-row"
key="100"
>
<div
className="panel-options-group"
className="thresholds-row-add-button"
onClick={[Function]}
>
<i
className="fa fa-plus"
/>
</div>
<div
className="thresholds-row-color-indicator"
style={
Object {
"backgroundColor": "#73BF69",
}
}
/>
<div
className="thresholds-row-input"
>
<div
className="panel-options-group__header"
className="thresholds-row-input-inner"
>
<span
className="panel-options-group__title"
>
Thresholds
</span>
</div>
<div
className="panel-options-group__body"
>
className="thresholds-row-input-inner-arrow"
/>
<div
className="thresholds"
className="thresholds-row-input-inner-color"
>
<div
className="thresholds-row"
key="100"
className="thresholds-row-input-inner-color-colorpicker"
>
<div
className="thresholds-row-add-button"
onClick={[Function]}
<WithTheme(ColorPicker)
color="green"
enableNamedColors={true}
onChange={[Function]}
>
<i
className="fa fa-plus"
/>
</div>
<div
className="thresholds-row-color-indicator"
style={
Object {
"backgroundColor": "#73BF69",
<ColorPicker
color="green"
enableNamedColors={true}
onChange={[Function]}
theme={
Object {
"type": "dark",
}
}
}
/>
<div
className="thresholds-row-input"
>
<div
className="thresholds-row-input-inner"
>
<span
className="thresholds-row-input-inner-arrow"
/>
<div
className="thresholds-row-input-inner-color"
>
<div
className="thresholds-row-input-inner-color-colorpicker"
>
<WithTheme(ColorPicker)
<PopoverController
content={
<ColorPickerPopover
color="green"
enableNamedColors={true}
onChange={[Function]}
>
<ColorPicker
color="green"
enableNamedColors={true}
onChange={[Function]}
theme={
Object {
"type": "dark",
}
theme={
Object {
"type": "dark",
}
>
<PopoverController
content={
<ColorPickerPopover
color="green"
enableNamedColors={true}
onChange={[Function]}
theme={
Object {
"type": "dark",
}
}
/>
}
hideAfter={300}
>
<ForwardRef(ColorPickerTrigger)
color="#73BF69"
onClick={[Function]}
onMouseLeave={[Function]}
>
<div
onClick={[Function]}
onMouseLeave={[Function]}
style={
Object {
"background": "inherit",
"border": "none",
"borderRadius": 10,
"color": "inherit",
"cursor": "pointer",
"overflow": "hidden",
"padding": 0,
}
}
>
<div
style={
Object {
"backgroundImage": "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)",
"border": "none",
"float": "left",
"height": 15,
"margin": 0,
"position": "relative",
"width": 15,
"zIndex": 0,
}
}
>
<div
style={
Object {
"backgroundColor": "#73BF69",
"bottom": 0,
"display": "block",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</div>
</div>
</ForwardRef(ColorPickerTrigger)>
</PopoverController>
</ColorPicker>
</WithTheme(ColorPicker)>
</div>
</div>
<div
className="thresholds-row-input-inner-value"
}
/>
}
hideAfter={300}
>
<Input
className=""
readOnly={true}
type="text"
value="Base"
<ForwardRef(ColorPickerTrigger)
color="#73BF69"
onClick={[Function]}
onMouseLeave={[Function]}
>
<div
onClick={[Function]}
onMouseLeave={[Function]}
style={
Object {
"flexGrow": 1,
"background": "inherit",
"border": "none",
"borderRadius": 10,
"color": "inherit",
"cursor": "pointer",
"overflow": "hidden",
"padding": 0,
}
}
>
<input
className="gf-form-input"
readOnly={true}
type="text"
value="Base"
/>
<div
style={
Object {
"backgroundImage": "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)",
"border": "none",
"float": "left",
"height": 15,
"margin": 0,
"position": "relative",
"width": 15,
"zIndex": 0,
}
}
>
<div
style={
Object {
"backgroundColor": "#73BF69",
"bottom": 0,
"display": "block",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</div>
</div>
</Input>
</div>
</div>
</div>
</ForwardRef(ColorPickerTrigger)>
</PopoverController>
</ColorPicker>
</WithTheme(ColorPicker)>
</div>
</div>
<div
className="thresholds-row-input-inner-value"
>
<Input
className=""
readOnly={true}
type="text"
value="Base"
>
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input"
readOnly={true}
type="text"
value="Base"
/>
</div>
</Input>
</div>
</div>
</div>
</Component>
</ThresholdsEditor>
</div>
</div>
`;