mirror of
https://github.com/grafana/grafana.git
synced 2026-08-19 01:34:54 -05:00
FieldConfig: add thresholds and color modes (#21273)
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+34
-14
@@ -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
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+121
-147
@@ -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>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user