Commit 14caa6a0 by Ryan McKinley Committed by GitHub

FieldDisplay: move threshold and mapping to Field (#17043)

parent acc678c3
import { Threshold } from './threshold';
import { ValueMapping } from './valueMapping';
export enum LoadingState {
NotStarted = 'NotStarted',
Loading = 'Loading',
......@@ -49,6 +52,12 @@ export interface Field {
decimals?: number | null; // Significant digits (for display)
min?: number | null;
max?: number | null;
// Convert input values into a display value
mappings?: ValueMapping[];
// Must be sorted by 'value', first value is always -Infinity
thresholds?: Threshold[];
}
export interface Labels {
......
export interface Threshold {
index: number;
value: number;
color: string;
}
import { Threshold } from '../types';
export function getThresholdForValue(
thresholds: Threshold[],
value: number | null | string | undefined
): Threshold | null {
if (thresholds.length === 1) {
return thresholds[0];
}
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
if (atThreshold) {
return atThreshold;
}
const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
if (belowThreshold.length > 0) {
const nearestThreshold = belowThreshold.sort((t1: Threshold, t2: Threshold) => t2.value - t1.value)[0];
return nearestThreshold;
export function getActiveThreshold(value: number, thresholds: Threshold[]): Threshold {
let active = thresholds[0];
for (const threshold of thresholds) {
if (value >= threshold.value) {
active = threshold;
} else {
break;
}
}
return active;
}
return null;
/**
* Sorts the thresholds
*/
export function sortThresholds(thresholds: Threshold[]) {
return thresholds.sort((t1, t2) => {
return t1.value - t2.value;
});
}
......@@ -49,9 +49,9 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
orientation: VizOrientation.Vertical,
displayMode: 'basic',
thresholds: [
{ index: 0, value: -Infinity, color: 'green' },
{ index: 1, value: threshold1Value, color: threshold1Color },
{ index: 1, value: threshold2Value, color: threshold2Color },
{ value: -Infinity, color: 'green' },
{ value: threshold1Value, color: threshold1Color },
{ value: threshold2Value, color: threshold2Color },
],
};
......
......@@ -25,11 +25,7 @@ function getProps(propOverrides?: Partial<Props>): Props {
maxValue: 100,
minValue: 0,
displayMode: 'basic',
thresholds: [
{ index: 0, value: -Infinity, color: 'green' },
{ index: 1, value: 70, color: 'orange' },
{ index: 2, value: 90, color: 'red' },
],
thresholds: [{ value: -Infinity, color: 'green' }, { value: 70, color: 'orange' }, { value: 90, color: 'red' }],
height: 300,
width: 300,
value: {
......
......@@ -7,7 +7,7 @@ import { getColorFromHexRgbOrName } from '../../utils';
// Types
import { DisplayValue, Themeable, VizOrientation } from '../../types';
import { Threshold, TimeSeriesValue, getThresholdForValue } from '@grafana/data';
import { Threshold, TimeSeriesValue, getActiveThreshold } from '@grafana/data';
const MIN_VALUE_HEIGHT = 18;
const MAX_VALUE_HEIGHT = 50;
......@@ -87,8 +87,14 @@ export class BarGauge extends PureComponent<Props> {
getCellColor(positionValue: TimeSeriesValue): CellColors {
const { thresholds, theme, value } = this.props;
const activeThreshold = getThresholdForValue(thresholds, positionValue);
if (positionValue === null) {
return {
background: 'gray',
border: 'gray',
};
}
const activeThreshold = getActiveThreshold(positionValue, thresholds);
if (activeThreshold !== null) {
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
......@@ -474,7 +480,7 @@ export function getBarGradient(props: Props, maxSize: number): string {
export function getValueColor(props: Props): string {
const { thresholds, theme, value } = props;
const activeThreshold = getThresholdForValue(thresholds, value.numeric);
const activeThreshold = getActiveThreshold(value.numeric, thresholds);
if (activeThreshold !== null) {
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
......
......@@ -14,7 +14,7 @@ const setup = (propOverrides?: object) => {
minValue: 0,
showThresholdMarkers: true,
showThresholdLabels: false,
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
thresholds: [{ value: -Infinity, color: '#7EB26D' }],
height: 300,
width: 300,
value: {
......@@ -48,9 +48,9 @@ describe('Get thresholds formatted', () => {
it('should get the correct formatted values when thresholds are added', () => {
const { instance } = setup({
thresholds: [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
});
......
......@@ -43,12 +43,12 @@ export class Gauge extends PureComponent<Props> {
const lastThreshold = thresholds[thresholds.length - 1];
return [
...thresholds.map(threshold => {
if (threshold.index === 0) {
...thresholds.map((threshold, index) => {
if (index === 0) {
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
}
const previousThreshold = thresholds[threshold.index - 1];
const previousThreshold = thresholds[index - 1];
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
}),
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
......
......@@ -3,7 +3,7 @@ import omit from 'lodash/omit';
import { VizOrientation, PanelModel } from '../../types/panel';
import { FieldDisplayOptions } from '../../utils/fieldDisplay';
import { Field, getFieldReducers } from '@grafana/data';
import { Field, getFieldReducers, Threshold, sortThresholds } from '@grafana/data';
export interface SingleStatBaseOptions {
fieldOptions: FieldDisplayOptions;
......@@ -39,18 +39,16 @@ export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseO
const { valueOptions } = old;
const fieldOptions = (old.fieldOptions = {} as FieldDisplayOptions);
fieldOptions.mappings = old.valueMappings;
fieldOptions.thresholds = old.thresholds;
const field = (fieldOptions.defaults = {} as Field);
if (valueOptions) {
field.unit = valueOptions.unit;
field.decimals = valueOptions.decimals;
field.mappings = old.valueMappings;
field.thresholds = migrateOldThresholds(old.thresholds);
field.unit = valueOptions.unit;
field.decimals = valueOptions.decimals;
// Make sure the stats have a valid name
if (valueOptions.stat) {
fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
}
// Make sure the stats have a valid name
if (valueOptions.stat) {
fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
}
field.min = old.minValue;
......@@ -58,7 +56,33 @@ export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseO
// remove old props
return omit(old, 'valueMappings', 'thresholds', 'valueOptions', 'minValue', 'maxValue');
} else if (old.fieldOptions) {
// Move mappins & thresholds to field defautls (6.4+)
const { mappings, thresholds, ...fieldOptions } = old.fieldOptions;
fieldOptions.defaults = {
mappings,
thresholds: migrateOldThresholds(thresholds),
...fieldOptions.defaults,
};
old.fieldOptions = fieldOptions;
return old;
}
return panel.options;
};
export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefined {
if (!thresholds || !thresholds.length) {
return undefined;
}
const copy = thresholds.map(t => {
return {
// Drops 'index'
value: t.value === null ? -Infinity : t.value,
color: t.color,
};
});
sortThresholds(copy);
copy[0].value = -Infinity;
return copy;
}
import React, { ChangeEvent } from 'react';
import { mount } from 'enzyme';
import { ThresholdsEditor, Props } from './ThresholdsEditor';
import { ThresholdsEditor, Props, threshodsWithoutKey } from './ThresholdsEditor';
import { colors } from '../../utils';
const setup = (propOverrides?: Partial<Props>) => {
......@@ -20,6 +20,10 @@ const setup = (propOverrides?: Partial<Props>) => {
};
};
function getCurrentThresholds(editor: ThresholdsEditor) {
return threshodsWithoutKey(editor.state.thresholds);
}
describe('Render', () => {
it('should render with base threshold', () => {
const { wrapper } = setup();
......@@ -32,60 +36,55 @@ describe('Initialization', () => {
it('should add a base threshold if missing', () => {
const { instance } = setup();
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]);
expect(getCurrentThresholds(instance)).toEqual([{ value: -Infinity, color: colors[0] }]);
});
});
describe('Add threshold', () => {
it('should not add threshold at index 0', () => {
const { instance } = setup();
instance.onAddThreshold(0);
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]);
});
it('should add threshold', () => {
const { instance } = setup();
instance.onAddThreshold(1);
instance.onAddThresholdAfter(instance.state.thresholds[0]);
expect(instance.state.thresholds).toEqual([
{ index: 0, value: -Infinity, color: colors[0] },
{ index: 1, value: 50, color: colors[2] },
expect(getCurrentThresholds(instance)).toEqual([
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
]);
});
it('should add another threshold above a first', () => {
const { instance } = setup({
thresholds: [{ index: 0, value: -Infinity, color: colors[0] }, { index: 1, value: 50, color: colors[2] }],
thresholds: [
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
],
});
instance.onAddThreshold(2);
instance.onAddThresholdAfter(instance.state.thresholds[1]);
expect(instance.state.thresholds).toEqual([
{ index: 0, value: -Infinity, color: colors[0] },
{ index: 1, value: 50, color: colors[2] },
{ index: 2, value: 75, color: colors[3] },
expect(getCurrentThresholds(instance)).toEqual([
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
{ value: 75, color: colors[3] }, // 2
]);
});
it('should add another threshold between first and second index', () => {
const { instance } = setup({
thresholds: [
{ index: 0, value: -Infinity, color: colors[0] },
{ index: 1, value: 50, color: colors[2] },
{ index: 2, value: 75, color: colors[3] },
{ value: -Infinity, color: colors[0] },
{ value: 50, color: colors[2] },
{ value: 75, color: colors[3] },
],
});
instance.onAddThreshold(2);
instance.onAddThresholdAfter(instance.state.thresholds[1]);
expect(instance.state.thresholds).toEqual([
{ index: 0, value: -Infinity, color: colors[0] },
{ index: 1, value: 50, color: colors[2] },
{ index: 2, value: 62.5, color: colors[4] },
{ index: 3, value: 75, color: colors[3] },
expect(getCurrentThresholds(instance)).toEqual([
{ value: -Infinity, color: colors[0] },
{ value: 50, color: colors[2] },
{ value: 62.5, color: colors[4] },
{ value: 75, color: colors[3] },
]);
});
});
......@@ -93,30 +92,30 @@ describe('Add threshold', () => {
describe('Remove threshold', () => {
it('should not remove threshold at index 0', () => {
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
];
const { instance } = setup({ thresholds });
instance.onRemoveThreshold(thresholds[0]);
instance.onRemoveThreshold(instance.state.thresholds[0]);
expect(instance.state.thresholds).toEqual(thresholds);
expect(getCurrentThresholds(instance)).toEqual(thresholds);
});
it('should remove threshold', () => {
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
];
const { instance } = setup({ thresholds });
instance.onRemoveThreshold(thresholds[1]);
instance.onRemoveThreshold(instance.state.thresholds[1]);
expect(instance.state.thresholds).toEqual([
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 75, color: '#6ED0E0' },
expect(getCurrentThresholds(instance)).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 75, color: '#6ED0E0' },
]);
});
});
......@@ -124,25 +123,25 @@ describe('Remove threshold', () => {
describe('change threshold value', () => {
it('should not change threshold at index 0', () => {
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
{ 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, thresholds[0]);
instance.onChangeThresholdValue(mockEvent, instance.state.thresholds[0]);
expect(instance.state.thresholds).toEqual(thresholds);
expect(getCurrentThresholds(instance)).toEqual(thresholds);
});
it('should update value', () => {
const { instance } = setup();
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 50, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
];
instance.state = {
......@@ -153,10 +152,10 @@ describe('change threshold value', () => {
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
expect(instance.state.thresholds).toEqual([
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 78, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
expect(getCurrentThresholds(instance)).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 78, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
]);
});
});
......@@ -165,9 +164,9 @@ describe('on blur threshold value', () => {
it('should resort rows and update indexes', () => {
const { instance } = setup();
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 78, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 78, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
];
instance.setState({
......@@ -176,10 +175,10 @@ describe('on blur threshold value', () => {
instance.onBlur();
expect(instance.state.thresholds).toEqual([
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 75, color: '#6ED0E0' },
{ index: 2, value: 78, color: '#EAB839' },
expect(getCurrentThresholds(instance)).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 75, color: '#6ED0E0' },
{ value: 78, color: '#EAB839' },
]);
});
});
import React, { PureComponent, ChangeEvent } from 'react';
import { Threshold } from '@grafana/data';
import { Threshold, sortThresholds } from '@grafana/data';
import { colors } from '../../utils';
import { ThemeContext } from '../../themes';
import { getColorFromHexRgbOrName } from '../../utils';
......@@ -13,115 +13,121 @@ export interface Props {
}
interface State {
thresholds: Threshold[];
thresholds: ThresholdWithKey[];
}
interface ThresholdWithKey extends Threshold {
key: number;
}
let counter = 100;
export class ThresholdsEditor extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const addDefaultThreshold = this.props.thresholds.length === 0;
const thresholds: Threshold[] = addDefaultThreshold
? [{ index: 0, value: -Infinity, color: colors[0] }]
: props.thresholds;
const thresholds = props.thresholds
? props.thresholds.map(t => {
return {
color: t.color,
value: t.value === null ? -Infinity : t.value,
key: counter++,
};
})
: ([] as ThresholdWithKey[]);
let needsCallback = false;
if (!thresholds.length) {
thresholds.push({ value: -Infinity, color: colors[0], key: counter++ });
needsCallback = true;
} else {
// First value is always base
thresholds[0].value = -Infinity;
}
// Update the state
this.state = { thresholds };
if (addDefaultThreshold) {
if (needsCallback) {
this.onChange();
}
}
onAddThreshold = (index: number) => {
onAddThresholdAfter = (threshold: ThresholdWithKey) => {
const { thresholds } = this.state;
const maxValue = 100;
const minValue = 0;
if (index === 0) {
return;
}
const newThresholds = thresholds.map(threshold => {
if (threshold.index >= index) {
const index = threshold.index + 1;
threshold = { ...threshold, index };
let prev: ThresholdWithKey | undefined = undefined;
let next: ThresholdWithKey | undefined = undefined;
for (const t of thresholds) {
if (prev && prev.key === threshold.key) {
next = t;
break;
}
return threshold;
});
prev = t;
}
// Setting value to a value between the previous thresholds
const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0];
const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0];
const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue;
const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue;
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
const prevValue = prev && isFinite(prev.value) ? prev.value : minValue;
const nextValue = next && isFinite(next.value) ? next.value : maxValue;
// Set a color
const color = colors.filter(c => !newThresholds.some(t => t.color === c))[1];
const color = colors.filter(c => !thresholds.some(t => t.color === c))[1];
const add = {
value: prevValue + (nextValue - prevValue) / 2.0,
color: color,
key: counter++,
};
const newThresholds = [...thresholds, add];
sortThresholds(newThresholds);
this.setState(
{
thresholds: this.sortThresholds([
...newThresholds,
{
color,
index,
value: value as number,
},
]),
thresholds: newThresholds,
},
() => this.onChange()
);
};
onRemoveThreshold = (threshold: Threshold) => {
if (threshold.index === 0) {
onRemoveThreshold = (threshold: ThresholdWithKey) => {
const { thresholds } = this.state;
if (!thresholds.length) {
return;
}
// Don't remove index 0
if (threshold.key === thresholds[0].key) {
return;
}
this.setState(
prevState => {
const newThresholds = prevState.thresholds.map(t => {
if (t.index > threshold.index) {
const index = t.index - 1;
t = { ...t, index };
}
return t;
});
return {
thresholds: newThresholds.filter(t => t !== threshold),
};
{
thresholds: thresholds.filter(t => t.key !== threshold.key),
},
() => this.onChange()
);
};
onChangeThresholdValue = (event: ChangeEvent<HTMLInputElement>, threshold: Threshold) => {
if (threshold.index === 0) {
return;
}
const { thresholds } = this.state;
onChangeThresholdValue = (event: ChangeEvent<HTMLInputElement>, threshold: ThresholdWithKey) => {
const cleanValue = event.target.value.replace(/,/g, '.');
const parsedValue = parseFloat(cleanValue);
const value = isNaN(parsedValue) ? '' : parsedValue;
const newThresholds = thresholds.map(t => {
if (t === threshold && t.index !== 0) {
const thresholds = this.state.thresholds.map(t => {
if (t.key === threshold.key) {
t = { ...t, value: value as number };
}
return t;
});
this.setState({ thresholds: newThresholds });
if (thresholds.length) {
thresholds[0].value = -Infinity;
}
this.setState({ thresholds });
};
onChangeThresholdColor = (threshold: Threshold, color: string) => {
onChangeThresholdColor = (threshold: ThresholdWithKey, color: string) => {
const { thresholds } = this.state;
const newThresholds = thresholds.map(t => {
if (t === threshold) {
if (t.key === threshold.key) {
t = { ...t, color: color };
}
......@@ -137,30 +143,22 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
};
onBlur = () => {
this.setState(prevState => {
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
let index = 0;
sortThresholds.forEach(t => {
t.index = index++;
});
return { thresholds: sortThresholds };
});
this.onChange();
const thresholds = [...this.state.thresholds];
sortThresholds(thresholds);
this.setState(
{
thresholds,
},
() => this.onChange()
);
};
onChange = () => {
this.props.onChange(this.state.thresholds);
};
sortThresholds = (thresholds: Threshold[]) => {
return thresholds.sort((t1, t2) => {
return t1.value - t2.value;
});
const { thresholds } = this.state;
this.props.onChange(threshodsWithoutKey(thresholds));
};
renderInput = (threshold: Threshold) => {
renderInput = (threshold: ThresholdWithKey) => {
return (
<div className="thresholds-row-input-inner">
<span className="thresholds-row-input-inner-arrow" />
......@@ -175,12 +173,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
</div>
)}
</div>
{threshold.index === 0 && (
{!isFinite(threshold.value) ? (
<div className="thresholds-row-input-inner-value">
<Input type="text" value="Base" readOnly />
</div>
)}
{threshold.index > 0 && (
) : (
<>
<div className="thresholds-row-input-inner-value">
<Input
......@@ -189,7 +186,6 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
onChange={(event: ChangeEvent<HTMLInputElement>) => this.onChangeThresholdValue(event, threshold)}
value={threshold.value}
onBlur={this.onBlur}
readOnly={threshold.index === 0}
/>
</div>
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
......@@ -212,13 +208,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
{thresholds
.slice(0)
.reverse()
.map((threshold, index) => {
.map(threshold => {
return (
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
<div
className="thresholds-row-add-button"
onClick={() => this.onAddThreshold(threshold.index + 1)}
>
<div className="thresholds-row" key={`${threshold.key}`}>
<div className="thresholds-row-add-button" onClick={() => this.onAddThresholdAfter(threshold)}>
<i className="fa fa-plus" />
</div>
<div
......@@ -237,3 +230,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
);
}
}
export function threshodsWithoutKey(thresholds: ThresholdWithKey[]): Threshold[] {
return thresholds.map(t => {
const { key, ...rest } = t;
return rest; // everything except key
});
}
......@@ -9,7 +9,6 @@ exports[`Render should render with base threshold 1`] = `
Array [
Object {
"color": "#7EB26D",
"index": 0,
"value": -Infinity,
},
],
......@@ -48,7 +47,7 @@ exports[`Render should render with base threshold 1`] = `
>
<div
className="thresholds-row"
key="0-0"
key="100"
>
<div
className="thresholds-row-add-button"
......
......@@ -103,7 +103,7 @@ describe('Format value', () => {
it('should return if value isNaN', () => {
const valueMappings: ValueMapping[] = [];
const value = 'N/A';
const instance = getDisplayProcessor({ mappings: valueMappings });
const instance = getDisplayProcessor({ field: { mappings: valueMappings } });
const result = instance(value);
......@@ -114,7 +114,7 @@ describe('Format value', () => {
const valueMappings: ValueMapping[] = [];
const value = '6';
const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
const result = instance(value);
......@@ -127,7 +127,7 @@ describe('Format value', () => {
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
];
const value = '10';
const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
const result = instance(value);
......@@ -160,7 +160,7 @@ describe('Format value', () => {
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '11';
const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
expect(instance(value).text).toEqual('1-20');
});
......
......@@ -7,16 +7,13 @@ import { getColorFromHexRgbOrName } from './namedColorsPalette';
// Types
import { DecimalInfo, DisplayValue, GrafanaTheme, GrafanaThemeType, DecimalCount } from '../types';
import { DateTime, dateTime, Threshold, ValueMapping, getMappedValue, Field } from '@grafana/data';
import { DateTime, dateTime, Threshold, getMappedValue, Field } from '@grafana/data';
export type DisplayProcessor = (value: any) => DisplayValue;
export interface DisplayValueOptions {
field?: Partial<Field>;
mappings?: ValueMapping[];
thresholds?: Threshold[];
// Alternative to empty string
noValue?: string;
......@@ -31,7 +28,8 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce
const formatFunc = getValueFormat(field.unit || 'none');
return (value: any) => {
const { mappings, thresholds, theme } = options;
const { theme } = options;
const { mappings, thresholds } = field;
let color;
let text = _.toString(value);
......
import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
import { FieldType, ReducerID } from '@grafana/data';
import { FieldType, ReducerID, Threshold } from '@grafana/data';
import { GrafanaThemeType } from '../types/theme';
import { getTheme } from '../themes/index';
......@@ -55,8 +55,6 @@ describe('FieldDisplay', () => {
},
fieldOptions: {
calcs: [],
mappings: [],
thresholds: [],
override: {},
defaults: {},
},
......@@ -68,8 +66,6 @@ describe('FieldDisplay', () => {
...options,
fieldOptions: {
calcs: [ReducerID.first],
mappings: [],
thresholds: [],
override: {},
defaults: {
title: '$__cell_0 * $__field_name * $__series_name',
......@@ -88,8 +84,6 @@ describe('FieldDisplay', () => {
...options,
fieldOptions: {
calcs: [ReducerID.last],
mappings: [],
thresholds: [],
override: {},
defaults: {},
},
......@@ -104,8 +98,6 @@ describe('FieldDisplay', () => {
values: true, //
limit: 1000,
calcs: [],
mappings: [],
thresholds: [],
override: {},
defaults: {},
},
......@@ -120,12 +112,27 @@ describe('FieldDisplay', () => {
values: true, //
limit: 2,
calcs: [],
mappings: [],
thresholds: [],
override: {},
defaults: {},
},
});
expect(display.map(v => v.display.numeric)).toEqual([1, 3]); // First 2 are from the first field
});
it('should restore -Infinity value for base threshold', () => {
const field = getFieldProperties({
thresholds: [
({
color: '#73BF69',
value: null,
} as unknown) as Threshold,
{
color: '#F2495C',
value: 50,
},
],
});
expect(field.thresholds!.length).toEqual(2);
expect(field.thresholds![0].value).toBe(-Infinity);
});
});
......@@ -4,16 +4,7 @@ import toString from 'lodash/toString';
import { DisplayValue, GrafanaTheme, InterpolateFunction, ScopedVars, GraphSeriesValue } from '../types/index';
import { getDisplayProcessor } from './displayValue';
import { getFlotPairs } from './flotPairs';
import {
ValueMapping,
Threshold,
ReducerID,
reduceField,
FieldType,
NullValueMode,
DataFrame,
Field,
} from '@grafana/data';
import { ReducerID, reduceField, FieldType, NullValueMode, DataFrame, Field } from '@grafana/data';
export interface FieldDisplayOptions {
values?: boolean; // If true show each row value
......@@ -22,10 +13,6 @@ export interface FieldDisplayOptions {
defaults: Partial<Field>; // Use these values unless otherwise stated
override: Partial<Field>; // Set these values regardless of the source
// Could these be data driven also?
thresholds: Threshold[];
mappings: ValueMapping[];
}
export const VAR_SERIES_NAME = '__series_name';
......@@ -127,8 +114,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
const display = getDisplayProcessor({
field,
mappings: fieldOptions.mappings,
thresholds: fieldOptions.thresholds,
theme: options.theme,
});
......@@ -263,6 +248,11 @@ export function getFieldProperties(...props: PartialField[]): Field {
field = applyFieldProperties(field, props[i]);
}
// First value is always -Infinity
if (field.thresholds && field.thresholds.length) {
field.thresholds[0].value = -Infinity;
}
// Verify that max > min
if (field.hasOwnProperty('min') && field.hasOwnProperty('max') && field.min! > field.max!) {
return {
......
......@@ -98,21 +98,6 @@ describe('PanelModel', () => {
expect(saveModel.events).toBe(undefined);
});
it('should restore -Infinity value for base threshold', () => {
expect(model.options.fieldOptions.thresholds).toEqual([
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: -Infinity,
},
]);
});
describe('when changing panel type', () => {
const newPanelPluginDefaults = {
showThresholdLabels: false,
......@@ -180,7 +165,7 @@ describe('PanelModel', () => {
it('should call react onPanelTypeChanged', () => {
expect(onPanelTypeChanged.mock.calls.length).toBe(1);
expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
expect(onPanelTypeChanged.mock.calls[0][2].fieldOptions.thresholds).toBeDefined();
expect(onPanelTypeChanged.mock.calls[0][2].fieldOptions).toBeDefined();
});
it('getQueryRunner() should return same instance after changing to another react panel', () => {
......
......@@ -136,7 +136,6 @@ export class PanelModel {
// queries must have refId
this.ensureQueryIds();
this.restoreInfintyForThresholds();
}
ensureQueryIds() {
......@@ -149,16 +148,6 @@ export class PanelModel {
}
}
restoreInfintyForThresholds() {
if (this.options && this.options.fieldOptions) {
for (const threshold of this.options.fieldOptions.thresholds) {
if (threshold.value === null) {
threshold.value = -Infinity;
}
}
}
}
getOptions() {
return this.options;
}
......
import { PanelModel } from '@grafana/ui';
import { barGaugePanelMigrationCheck } from './BarGaugeMigrations';
describe('BarGauge Panel Migrations', () => {
it('from 6.2', () => {
const panel = {
id: 7,
links: [],
options: {
displayMode: 'lcd',
fieldOptions: {
calcs: ['mean'],
defaults: {
decimals: null,
max: -22,
min: 33,
unit: 'watt',
},
mappings: [],
override: {},
thresholds: [
{
color: 'green',
index: 0,
value: null,
},
{
color: 'orange',
index: 1,
value: 40,
},
{
color: 'red',
index: 2,
value: 80,
},
],
values: false,
},
orientation: 'vertical',
},
pluginVersion: '6.2.0',
targets: [
{
refId: 'A',
scenarioId: 'random_walk',
},
{
refId: 'B',
scenarioId: 'random_walk',
},
],
timeFrom: null,
timeShift: null,
title: 'Usage',
type: 'bargauge',
} as PanelModel;
expect(barGaugePanelMigrationCheck(panel)).toMatchSnapshot();
});
});
import { PanelModel } from '@grafana/ui';
import {
sharedSingleStatMigrationCheck,
migrateOldThresholds,
} from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
import { BarGaugeOptions } from './types';
export const barGaugePanelMigrationCheck = (panel: PanelModel<BarGaugeOptions>): Partial<BarGaugeOptions> => {
if (!panel.options) {
// This happens on the first load or when migrating from angular
return {};
}
// Move thresholds to field
const previousVersion = panel.pluginVersion || '';
if (previousVersion.startsWith('6.2') || previousVersion.startsWith('6.3')) {
console.log('TRANSFORM', panel.options);
const old = panel.options as any;
const { fieldOptions } = old;
if (fieldOptions) {
const { mappings, thresholds, ...rest } = fieldOptions;
rest.defaults = {
mappings,
thresholds: migrateOldThresholds(thresholds),
...rest.defaults,
};
return {
...old,
fieldOptions: rest,
};
}
}
// Default to the standard migration path
return sharedSingleStatMigrationCheck(panel);
};
......@@ -14,7 +14,6 @@ import { PanelProps } from '@grafana/ui';
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
const { options } = this.props;
const { fieldOptions } = options;
const { field, display } = value;
return (
......@@ -23,7 +22,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
width={width}
height={height}
orientation={options.orientation}
thresholds={fieldOptions.thresholds}
thresholds={field.thresholds}
theme={config.theme}
itemSpacing={this.getItemSpacing()}
displayMode={options.displayMode}
......
......@@ -19,17 +19,21 @@ import { Threshold, ValueMapping } from '@grafana/data';
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
onThresholdsChanged = (thresholds: Threshold[]) =>
this.onDisplayOptionsChanged({
...this.props.options.fieldOptions,
onThresholdsChanged = (thresholds: Threshold[]) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
thresholds,
});
};
onValueMappingsChanged = (mappings: ValueMapping[]) =>
this.onDisplayOptionsChanged({
...this.props.options.fieldOptions,
onValueMappingsChanged = (mappings: ValueMapping[]) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
mappings,
});
};
onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
this.props.onOptionsChange({
......@@ -50,6 +54,7 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
render() {
const { options } = this.props;
const { fieldOptions } = options;
const { defaults } = fieldOptions;
const labelWidth = 6;
......@@ -80,13 +85,13 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
</div>
</PanelOptionsGroup>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={fieldOptions.defaults} />
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
</PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
</>
);
}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BarGauge Panel Migrations from 6.2 1`] = `
Object {
"displayMode": "lcd",
"fieldOptions": Object {
"calcs": Array [
"mean",
],
"defaults": Object {
"decimals": null,
"mappings": Array [],
"max": -22,
"min": 33,
"thresholds": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "orange",
"value": 40,
},
Object {
"color": "red",
"value": 80,
},
],
"unit": "watt",
},
"override": Object {},
"values": false,
},
"orientation": "vertical",
}
`;
......@@ -2,8 +2,10 @@ import { PanelPlugin, sharedSingleStatOptionsCheck } from '@grafana/ui';
import { BarGaugePanel } from './BarGaugePanel';
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
import { BarGaugeOptions, defaults } from './types';
import { barGaugePanelMigrationCheck } from './BarGaugeMigrations';
export const plugin = new PanelPlugin<BarGaugeOptions>(BarGaugePanel)
.setDefaults(defaults)
.setEditor(BarGaugePanelEditor)
.setPanelChangeHandler(sharedSingleStatOptionsCheck);
.setPanelChangeHandler(sharedSingleStatOptionsCheck)
.setMigrationHandler(barGaugePanelMigrationCheck);
import { Field, getFieldReducers } from '@grafana/data';
import { PanelModel } from '@grafana/ui';
import { GaugeOptions } from './types';
import { sharedSingleStatMigrationCheck } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
import {
sharedSingleStatMigrationCheck,
migrateOldThresholds,
} from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
import { FieldDisplayOptions } from '@grafana/ui/src/utils/fieldDisplay';
export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Partial<GaugeOptions> => {
......@@ -10,7 +13,8 @@ export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Parti
return {};
}
if (!panel.pluginVersion || panel.pluginVersion.startsWith('6.1')) {
const previousVersion = panel.pluginVersion || '';
if (!previousVersion || previousVersion.startsWith('6.1')) {
const old = panel.options as any;
const { valueOptions } = old;
......@@ -20,23 +24,36 @@ export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Parti
options.orientation = old.orientation;
const fieldOptions = (options.fieldOptions = {} as FieldDisplayOptions);
fieldOptions.mappings = old.valueMappings;
fieldOptions.thresholds = old.thresholds;
const field = (fieldOptions.defaults = {} as Field);
if (valueOptions) {
field.unit = valueOptions.unit;
field.decimals = valueOptions.decimals;
// Make sure the stats have a valid name
if (valueOptions.stat) {
fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
}
field.mappings = old.valueMappings;
field.thresholds = migrateOldThresholds(old.thresholds);
field.unit = valueOptions.unit;
field.decimals = valueOptions.decimals;
// Make sure the stats have a valid name
if (valueOptions.stat) {
fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
}
field.min = old.minValue;
field.max = old.maxValue;
return options;
} else if (previousVersion.startsWith('6.2') || previousVersion.startsWith('6.3')) {
const old = panel.options as any;
const { fieldOptions } = old;
if (fieldOptions) {
const { mappings, thresholds, ...rest } = fieldOptions;
rest.default = {
mappings,
thresholds: migrateOldThresholds(thresholds),
...rest.defaults,
};
return {
...old.options,
fieldOptions: rest,
};
}
}
// Default to the standard migration path
......
......@@ -14,7 +14,6 @@ import { PanelProps, VizRepeater } from '@grafana/ui';
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
const { options } = this.props;
const { fieldOptions } = options;
const { field, display } = value;
return (
......@@ -22,7 +21,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
value={display}
width={width}
height={height}
thresholds={fieldOptions.thresholds}
thresholds={field.thresholds}
showThresholdLabels={options.showThresholdLabels}
showThresholdMarkers={options.showThresholdMarkers}
minValue={field.min}
......
......@@ -27,17 +27,21 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
showThresholdMarkers: !this.props.options.showThresholdMarkers,
});
onThresholdsChanged = (thresholds: Threshold[]) =>
this.onDisplayOptionsChanged({
...this.props.options.fieldOptions,
onThresholdsChanged = (thresholds: Threshold[]) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
thresholds,
});
};
onValueMappingsChanged = (mappings: ValueMapping[]) =>
this.onDisplayOptionsChanged({
...this.props.options.fieldOptions,
onValueMappingsChanged = (mappings: ValueMapping[]) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
mappings,
});
};
onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
this.props.onOptionsChange({
......@@ -55,6 +59,7 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
render() {
const { options } = this.props;
const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
const { defaults } = fieldOptions;
return (
<>
......@@ -80,13 +85,13 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
</PanelOptionsGroup>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={fieldOptions.defaults} />
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
</PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
</>
);
}
......
......@@ -8,43 +8,39 @@ Object {
],
"defaults": Object {
"decimals": 3,
"mappings": Array [
Object {
"from": "50",
"id": 1,
"operator": "",
"text": "BIG",
"to": "1000",
"type": 2,
"value": "",
},
],
"max": "50",
"min": "-50",
"thresholds": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "#EAB839",
"value": -25,
},
Object {
"color": "#6ED0E0",
"value": 0,
},
Object {
"color": "red",
"value": 25,
},
],
"unit": "accMS2",
},
"mappings": Array [
Object {
"from": "50",
"id": 1,
"operator": "",
"text": "BIG",
"to": "1000",
"type": 2,
"value": "",
},
],
"thresholds": Array [
Object {
"color": "green",
"index": 0,
"value": null,
},
Object {
"color": "#EAB839",
"index": 1,
"value": -25,
},
Object {
"color": "#6ED0E0",
"index": 2,
"value": 0,
},
Object {
"color": "red",
"index": 3,
"value": 25,
},
],
},
"orientation": "auto",
"showThresholdLabels": true,
......
......@@ -14,11 +14,13 @@ import { PieChartOptionsBox } from './PieChartOptionsBox';
import { PieChartOptions } from './types';
export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
onValueMappingsChanged = (mappings: ValueMapping[]) =>
this.onDisplayOptionsChanged({
...this.props.options.fieldOptions,
onValueMappingsChanged = (mappings: ValueMapping[]) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
mappings,
});
};
onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
this.props.onOptionsChange({
......@@ -36,6 +38,7 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
render() {
const { onOptionsChange, options } = this.props;
const { fieldOptions } = options;
const { defaults } = fieldOptions;
return (
<>
......@@ -45,13 +48,13 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
</PanelOptionsGroup>
<PanelOptionsGroup title="Field (default)">
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={fieldOptions.defaults} />
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
</PanelOptionsGroup>
<PieChartOptionsBox onOptionsChange={onOptionsChange} options={options} />
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
</>
);
}
......
......@@ -18,17 +18,21 @@ import { FontSizeEditor } from './FontSizeEditor';
import { SparklineEditor } from './SparklineEditor';
export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
onThresholdsChanged = (thresholds: Threshold[]) =>
this.onDisplayOptionsChanged({
...this.props.options.fieldOptions,
onThresholdsChanged = (thresholds: Threshold[]) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
thresholds,
});
};
onValueMappingsChanged = (mappings: ValueMapping[]) =>
this.onDisplayOptionsChanged({
...this.props.options.fieldOptions,
onValueMappingsChanged = (mappings: ValueMapping[]) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
mappings,
});
};
onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
this.props.onOptionsChange({
......@@ -52,6 +56,7 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
render() {
const { options } = this.props;
const { fieldOptions } = options;
const { defaults } = fieldOptions;
return (
<>
......@@ -61,17 +66,17 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
</PanelOptionsGroup>
<PanelOptionsGroup title="Field (default)">
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={fieldOptions.defaults} />
<FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
</PanelOptionsGroup>
<FontSizeEditor options={options} onChange={this.props.onOptionsChange} />
<ColoringEditor options={options} onChange={this.props.onOptionsChange} />
<SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
</>
);
}
......
......@@ -25,10 +25,15 @@ export interface SingleStatOptions extends SingleStatBaseOptions {
export const standardFieldDisplayOptions: FieldDisplayOptions = {
values: false,
calcs: [ReducerID.mean],
defaults: {},
defaults: {
min: 0,
max: 100,
thresholds: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' }, // 80%
],
},
override: {},
mappings: [],
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
};
export const defaults: SingleStatOptions = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment