mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Refactor: move threshold editor out of grafana-ui (#44300)
This commit is contained in:
@@ -5,11 +5,16 @@ import {
|
||||
FieldType,
|
||||
standardEditorsRegistry,
|
||||
StandardEditorsRegistryItem,
|
||||
ThresholdsConfig,
|
||||
ThresholdsFieldConfigSettings,
|
||||
ThresholdsMode,
|
||||
thresholdsOverrideProcessor,
|
||||
ValueMapping,
|
||||
ValueMappingFieldConfigSettings,
|
||||
valueMappingsOverrideProcessor,
|
||||
} from '@grafana/data';
|
||||
import { ValueMappingsValueEditor } from 'app/features/dimensions/editors/ValueMappingsEditor/mappings';
|
||||
import { ThresholdsValueEditor } from 'app/features/dimensions/editors/ThresholdsEditor/thresholds';
|
||||
|
||||
/**
|
||||
* Returns collection of standard option editors definitions
|
||||
@@ -29,7 +34,14 @@ export const getAllOptionEditors = () => {
|
||||
editor: ValueMappingsValueEditor as any,
|
||||
};
|
||||
|
||||
return [...getStandardOptionEditors(), dashboardPicker, mappings];
|
||||
const thresholds: StandardEditorsRegistryItem<ThresholdsConfig> = {
|
||||
id: 'thresholds',
|
||||
name: 'Thresholds',
|
||||
description: 'Allows defining thresholds',
|
||||
editor: ThresholdsValueEditor as any,
|
||||
};
|
||||
|
||||
return [...getStandardOptionEditors(), dashboardPicker, mappings, thresholds];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -52,5 +64,25 @@ export const getAllStandardFieldConfigs = () => {
|
||||
getItemsCount: (value?) => (value ? value.length : 0),
|
||||
};
|
||||
|
||||
return [...getStandardFieldConfigs(), mappings];
|
||||
const thresholds: FieldConfigPropertyItem<any, ThresholdsConfig, ThresholdsFieldConfigSettings> = {
|
||||
id: 'thresholds',
|
||||
path: 'thresholds',
|
||||
name: 'Thresholds',
|
||||
editor: standardEditorsRegistry.get('thresholds').editor as any,
|
||||
override: standardEditorsRegistry.get('thresholds').editor as any,
|
||||
process: thresholdsOverrideProcessor,
|
||||
settings: {},
|
||||
defaultValue: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: 80, color: 'red' },
|
||||
],
|
||||
},
|
||||
shouldApply: () => true,
|
||||
category: ['Thresholds'],
|
||||
getItemsCount: (value) => (value ? value.steps.length : 0),
|
||||
};
|
||||
|
||||
return [...getStandardFieldConfigs(), mappings, thresholds];
|
||||
};
|
||||
|
||||
@@ -16,11 +16,11 @@ import { DashboardModel, PanelModel } from '../../state';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
||||
import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui';
|
||||
import { dataOverrideTooltipDescription, overrideRuleTooltipDescription } from './state/getOptionOverrides';
|
||||
import { getAllOptionEditors, getAllStandardFieldConfigs } from 'app/core/components/editors/registry';
|
||||
|
||||
standardEditorsRegistry.setInit(getStandardOptionEditors);
|
||||
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
||||
standardEditorsRegistry.setInit(getAllOptionEditors);
|
||||
standardFieldConfigEditorRegistry.setInit(getAllStandardFieldConfigs);
|
||||
|
||||
const mockStore = configureMockStore<any, any>();
|
||||
const OptionsPaneSelector = selectors.components.PanelEditor.OptionsPane;
|
||||
@@ -126,6 +126,7 @@ describe('OptionsPaneOptions', () => {
|
||||
|
||||
expect(screen.getByRole('heading', { name: /Panel options/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /Standard options/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /Value mappings/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /Thresholds/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /TestPanel/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { createTheme, ThresholdsMode } from '@grafana/data';
|
||||
import { ThresholdsEditor, Props, thresholdsWithoutKey } from './ThresholdsEditor';
|
||||
import { mockThemeContext, colors } from '@grafana/ui';
|
||||
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
onChange: jest.fn(),
|
||||
thresholds: { mode: ThresholdsMode.Absolute, steps: [] },
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = mount(<ThresholdsEditor {...props} />);
|
||||
const instance = wrapper.instance() as ThresholdsEditor;
|
||||
|
||||
return {
|
||||
instance,
|
||||
wrapper,
|
||||
};
|
||||
};
|
||||
|
||||
function getCurrentThresholds(editor: ThresholdsEditor) {
|
||||
return thresholdsWithoutKey(editor.props.thresholds, editor.state.steps);
|
||||
}
|
||||
|
||||
describe('ThresholdsEditor', () => {
|
||||
let restoreThemeContext: any;
|
||||
|
||||
beforeAll(() => {
|
||||
restoreThemeContext = mockThemeContext(createTheme());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
restoreThemeContext();
|
||||
});
|
||||
|
||||
it('should render with base threshold', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper.find('input').length).toBe(3);
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should add a base threshold if missing', () => {
|
||||
const { instance } = setup();
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([{ value: -Infinity, color: 'green' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add threshold', () => {
|
||||
it('should add threshold', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
instance.onAddThreshold();
|
||||
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: 'green' }, // 0
|
||||
{ value: 0, color: colors[1] }, // 1
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add another threshold above last', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: colors[0] }, // 0
|
||||
{ value: 50, color: colors[2] }, // 1
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
instance.onAddThreshold();
|
||||
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: colors[0] }, // 0
|
||||
{ value: 50, color: colors[2] }, // 1
|
||||
{ value: 60, color: colors[3] }, // 2
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove threshold', () => {
|
||||
it('should not remove threshold at index 0', () => {
|
||||
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.steps[0]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual(thresholds);
|
||||
});
|
||||
|
||||
it('should remove threshold', () => {
|
||||
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.steps[1]);
|
||||
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change threshold value', () => {
|
||||
it('should not change threshold at index 0', () => {
|
||||
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.steps[0]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual(thresholds);
|
||||
});
|
||||
|
||||
it('should update value', () => {
|
||||
const { instance } = setup();
|
||||
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 = {
|
||||
steps: thresholds.steps,
|
||||
};
|
||||
|
||||
const mockEvent = ({ target: { value: '78' } } as any) as ChangeEvent<HTMLInputElement>;
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, thresholds.steps[1]);
|
||||
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
{ value: 78, color: '#EAB839' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on blur threshold value', () => {
|
||||
it('should resort rows and update indexes', () => {
|
||||
const { instance } = setup();
|
||||
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({
|
||||
steps: thresholds.steps,
|
||||
});
|
||||
|
||||
instance.onBlur();
|
||||
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
{ value: 78, color: '#EAB839' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on load with invalid steps', () => {
|
||||
it('should exclude invalid steps and render a proper list', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: '#7EB26D', key: 1 },
|
||||
{ value: 75, color: '#6ED0E0', key: 2 },
|
||||
{ color: '#7EB26D', key: 3 } as any,
|
||||
{ value: 78, color: '#EAB839', key: 4 },
|
||||
{ value: null, color: '#7EB26D', key: 5 } as any,
|
||||
{ value: null, color: '#7EB26D', key: 6 } as any,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
{ value: 78, color: '#EAB839' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,336 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
Threshold,
|
||||
sortThresholds,
|
||||
ThresholdsConfig,
|
||||
ThresholdsMode,
|
||||
SelectableValue,
|
||||
GrafanaTheme,
|
||||
} from '@grafana/data';
|
||||
import { isNumber } from 'lodash';
|
||||
import {
|
||||
Input,
|
||||
colors,
|
||||
ColorPicker,
|
||||
Icon,
|
||||
ThemeContext,
|
||||
Button,
|
||||
Label,
|
||||
RadioButtonGroup,
|
||||
stylesFactory,
|
||||
} from '@grafana/ui';
|
||||
|
||||
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 {
|
||||
thresholds: ThresholdsConfig;
|
||||
onChange: (thresholds: ThresholdsConfig) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
steps: ThresholdWithKey[];
|
||||
}
|
||||
|
||||
export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
private latestThresholdInputRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const steps = toThresholdsWithKey(props.thresholds!.steps);
|
||||
steps[0].value = -Infinity;
|
||||
|
||||
this.state = { steps };
|
||||
this.latestThresholdInputRef = React.createRef();
|
||||
}
|
||||
|
||||
onAddThreshold = () => {
|
||||
const { steps } = this.state;
|
||||
|
||||
let nextValue = 0;
|
||||
|
||||
if (steps.length > 1) {
|
||||
nextValue = steps[steps.length - 1].value + 10;
|
||||
}
|
||||
|
||||
let color = colors.filter((c) => !steps.some((t) => t.color === c))[1];
|
||||
if (!color) {
|
||||
// Default color when all colors are used
|
||||
color = '#CCCCCC';
|
||||
}
|
||||
|
||||
const add = {
|
||||
value: nextValue,
|
||||
color: color,
|
||||
key: counter++,
|
||||
};
|
||||
|
||||
const newThresholds = [...steps, add];
|
||||
sortThresholds(newThresholds);
|
||||
|
||||
this.setState({ steps: newThresholds }, () => {
|
||||
if (this.latestThresholdInputRef.current) {
|
||||
this.latestThresholdInputRef.current.focus();
|
||||
}
|
||||
this.onChange();
|
||||
});
|
||||
};
|
||||
|
||||
onRemoveThreshold = (threshold: ThresholdWithKey) => {
|
||||
const { steps } = this.state;
|
||||
|
||||
if (!steps.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't remove index 0
|
||||
if (threshold.key === steps[0].key) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ steps: steps.filter((t) => t.key !== threshold.key) }, this.onChange);
|
||||
};
|
||||
|
||||
onChangeThresholdValue = (event: ChangeEvent<HTMLInputElement>, threshold: ThresholdWithKey) => {
|
||||
const cleanValue = event.target.value.replace(/,/g, '.');
|
||||
const parsedValue = parseFloat(cleanValue);
|
||||
const value = isNaN(parsedValue) ? '' : parsedValue;
|
||||
|
||||
const steps = this.state.steps.map((t) => {
|
||||
if (t.key === threshold.key) {
|
||||
t = { ...t, value: value as number };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
|
||||
if (steps.length) {
|
||||
steps[0].value = -Infinity;
|
||||
}
|
||||
|
||||
sortThresholds(steps);
|
||||
this.setState({ steps });
|
||||
};
|
||||
|
||||
onChangeThresholdColor = (threshold: ThresholdWithKey, color: string) => {
|
||||
const { steps } = this.state;
|
||||
|
||||
const newThresholds = steps.map((t) => {
|
||||
if (t.key === threshold.key) {
|
||||
t = { ...t, color: color };
|
||||
}
|
||||
|
||||
return t;
|
||||
});
|
||||
|
||||
this.setState({ steps: newThresholds }, this.onChange);
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
const steps = [...this.state.steps];
|
||||
sortThresholds(steps);
|
||||
this.setState({ steps }, this.onChange);
|
||||
};
|
||||
|
||||
onChange = () => {
|
||||
this.props.onChange(thresholdsWithoutKey(this.props.thresholds, this.state.steps));
|
||||
};
|
||||
|
||||
onModeChanged = (value?: ThresholdsMode) => {
|
||||
this.props.onChange({
|
||||
...this.props.thresholds,
|
||||
mode: value!,
|
||||
});
|
||||
};
|
||||
|
||||
renderInput(threshold: ThresholdWithKey, styles: ThresholdStyles, idx: number) {
|
||||
const isPercent = this.props.thresholds.mode === ThresholdsMode.Percentage;
|
||||
|
||||
const ariaLabel = `Threshold ${idx + 1}`;
|
||||
if (!isFinite(threshold.value)) {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={'Base'}
|
||||
aria-label={ariaLabel}
|
||||
disabled
|
||||
prefix={
|
||||
<div className={styles.colorPicker}>
|
||||
<ColorPicker
|
||||
color={threshold.color}
|
||||
onChange={(color) => this.onChangeThresholdColor(threshold, color)}
|
||||
enableNamedColors={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
key={isPercent.toString()}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => this.onChangeThresholdValue(event, threshold)}
|
||||
value={threshold.value}
|
||||
aria-label={ariaLabel}
|
||||
ref={idx === 0 ? this.latestThresholdInputRef : null}
|
||||
onBlur={this.onBlur}
|
||||
prefix={
|
||||
<div className={styles.inputPrefix}>
|
||||
<div className={styles.colorPicker}>
|
||||
<ColorPicker
|
||||
color={threshold.color}
|
||||
onChange={(color) => this.onChangeThresholdColor(threshold, color)}
|
||||
enableNamedColors={true}
|
||||
/>
|
||||
</div>
|
||||
{isPercent && <div className={styles.percentIcon}>%</div>}
|
||||
</div>
|
||||
}
|
||||
suffix={
|
||||
<Icon className={styles.trashIcon} name="trash-alt" onClick={() => this.onRemoveThreshold(threshold)} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { thresholds } = this.props;
|
||||
const { steps } = this.state;
|
||||
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{(theme) => {
|
||||
const styles = getStyles(theme.v1);
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Button
|
||||
size="sm"
|
||||
icon="plus"
|
||||
onClick={() => this.onAddThreshold()}
|
||||
variant="secondary"
|
||||
className={styles.addButton}
|
||||
fullWidth
|
||||
>
|
||||
Add threshold
|
||||
</Button>
|
||||
<div className={styles.thresholds}>
|
||||
{steps
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((threshold, idx) => (
|
||||
<div className={styles.item} key={`${threshold.key}`}>
|
||||
{this.renderInput(threshold, styles, idx)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label description="Percentage means thresholds relative to min & max">Thresholds mode</Label>
|
||||
<RadioButtonGroup options={modes} onChange={this.onModeChanged} value={thresholds.mode} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ThresholdWithKey extends Threshold {
|
||||
key: number;
|
||||
}
|
||||
|
||||
let counter = 100;
|
||||
|
||||
function toThresholdsWithKey(steps?: Threshold[]): ThresholdWithKey[] {
|
||||
if (!steps || steps.length === 0) {
|
||||
steps = [{ value: -Infinity, color: 'green' }];
|
||||
}
|
||||
|
||||
return steps
|
||||
.filter((t, i) => isNumber(t.value) || i === 0)
|
||||
.map((t) => {
|
||||
return {
|
||||
color: t.color,
|
||||
value: t.value === null ? -Infinity : t.value,
|
||||
key: counter++,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
interface ThresholdStyles {
|
||||
wrapper: string;
|
||||
thresholds: string;
|
||||
item: string;
|
||||
colorPicker: string;
|
||||
addButton: string;
|
||||
percentIcon: string;
|
||||
inputPrefix: string;
|
||||
trashIcon: string;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory(
|
||||
(theme: GrafanaTheme): ThresholdStyles => {
|
||||
return {
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
thresholds: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: ${theme.spacing.formSpacingBase * 2}px;
|
||||
`,
|
||||
item: css`
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
colorPicker: css`
|
||||
padding: 0 ${theme.spacing.sm};
|
||||
`,
|
||||
addButton: css`
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
percentIcon: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
color: ${theme.colors.textWeak};
|
||||
`,
|
||||
inputPrefix: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
trashIcon: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
`,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { FieldConfigEditorProps, ThresholdsConfig, ThresholdsMode, ThresholdsFieldConfigSettings } from '@grafana/data';
|
||||
import { ThresholdsEditor } from './ThresholdsEditor';
|
||||
|
||||
export class ThresholdsValueEditor extends React.PureComponent<
|
||||
FieldConfigEditorProps<ThresholdsConfig, ThresholdsFieldConfigSettings>
|
||||
> {
|
||||
constructor(props: FieldConfigEditorProps<ThresholdsConfig, ThresholdsFieldConfigSettings>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onChange } = this.props;
|
||||
let value = this.props.value;
|
||||
if (!value) {
|
||||
value = {
|
||||
mode: ThresholdsMode.Percentage,
|
||||
|
||||
// Must be sorted by 'value', first value is always -Infinity
|
||||
steps: [
|
||||
// anything?
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return <ThresholdsEditor thresholds={value} onChange={onChange} />;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user