mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
NewPanelEdit: Remove legacy editors for thresholds, value mappings and field properties (#23497)
* Remove legacy value mappings editor * Remove FieldPropertiesEditor * Remove legacy ThresholdsEditor
This commit is contained in:
@@ -1,32 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FieldPropertiesEditor } from './FieldPropertiesEditor';
|
|
||||||
import { FieldConfig } from '@grafana/data';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
|
|
||||||
describe('FieldPropertiesEditor', () => {
|
|
||||||
describe('when bluring min/max field', () => {
|
|
||||||
it("when title was modified it should persist it's value", () => {
|
|
||||||
const onChangeHandler = jest.fn();
|
|
||||||
const value: FieldConfig = {
|
|
||||||
title: 'Title set',
|
|
||||||
};
|
|
||||||
const container = mount(<FieldPropertiesEditor value={value} onChange={onChangeHandler} showTitle showMinMax />);
|
|
||||||
const minInput = container.find('input[aria-label="Field properties editor min input"]');
|
|
||||||
const maxInput = container.find('input[aria-label="Field properties editor max input"]');
|
|
||||||
|
|
||||||
// Simulating title update provided from PanelModel options
|
|
||||||
container.setProps({ value: { title: 'Title updated' } });
|
|
||||||
|
|
||||||
minInput.simulate('blur');
|
|
||||||
maxInput.simulate('blur');
|
|
||||||
|
|
||||||
expect(onChangeHandler).toHaveBeenLastCalledWith({
|
|
||||||
title: 'Title updated',
|
|
||||||
min: undefined,
|
|
||||||
max: undefined,
|
|
||||||
decimals: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
//
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// Libraries
|
|
||||||
import React, { ChangeEvent, useState, useCallback } from 'react';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import { FormField } from '../FormField/FormField';
|
|
||||||
import { FormLabel } from '../FormLabel/FormLabel';
|
|
||||||
import { UnitPicker } from '../UnitPicker/UnitPicker';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import {
|
|
||||||
VAR_SERIES_NAME,
|
|
||||||
VAR_FIELD_NAME,
|
|
||||||
VAR_CALC,
|
|
||||||
VAR_CELL_PREFIX,
|
|
||||||
toIntegerOrUndefined,
|
|
||||||
FieldConfig,
|
|
||||||
toFloatOrUndefined,
|
|
||||||
toNumberString,
|
|
||||||
} from '@grafana/data';
|
|
||||||
|
|
||||||
const labelWidth = 6;
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
showMinMax?: boolean;
|
|
||||||
showTitle?: boolean;
|
|
||||||
value: FieldConfig;
|
|
||||||
onChange: (value: FieldConfig, event?: React.SyntheticEvent<HTMLElement>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMinMax, showTitle }) => {
|
|
||||||
const { unit, title } = value;
|
|
||||||
|
|
||||||
const [decimals, setDecimals] = useState(
|
|
||||||
value.decimals !== undefined && value.decimals !== null ? value.decimals.toString() : ''
|
|
||||||
);
|
|
||||||
const [min, setMin] = useState(toNumberString(value.min));
|
|
||||||
const [max, setMax] = useState(toNumberString(value.max));
|
|
||||||
|
|
||||||
const onTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onChange({ ...value, title: event.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDecimalChange = useCallback(
|
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setDecimals(event.target.value);
|
|
||||||
},
|
|
||||||
[value.decimals, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onMinChange = useCallback(
|
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setMin(event.target.value);
|
|
||||||
},
|
|
||||||
[value.min, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onMaxChange = useCallback(
|
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setMax(event.target.value);
|
|
||||||
},
|
|
||||||
[value.max, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onUnitChange = (unit?: string) => {
|
|
||||||
onChange({ ...value, unit });
|
|
||||||
};
|
|
||||||
|
|
||||||
const commitChanges = useCallback(() => {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
decimals: toIntegerOrUndefined(decimals),
|
|
||||||
min: toFloatOrUndefined(min),
|
|
||||||
max: toFloatOrUndefined(max),
|
|
||||||
});
|
|
||||||
}, [min, max, decimals, value]);
|
|
||||||
|
|
||||||
const titleTooltip = (
|
|
||||||
<div>
|
|
||||||
Template Variables:
|
|
||||||
<br />
|
|
||||||
{'${' + VAR_SERIES_NAME + '}'}
|
|
||||||
<br />
|
|
||||||
{'${' + VAR_FIELD_NAME + '}'}
|
|
||||||
<br />
|
|
||||||
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{showTitle && (
|
|
||||||
<FormField
|
|
||||||
label="Title"
|
|
||||||
labelWidth={labelWidth}
|
|
||||||
onChange={onTitleChange}
|
|
||||||
value={title}
|
|
||||||
tooltip={titleTooltip}
|
|
||||||
placeholder="Auto"
|
|
||||||
aria-label="Field properties editor title input"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="gf-form">
|
|
||||||
<FormLabel width={labelWidth}>Unit</FormLabel>
|
|
||||||
<UnitPicker value={unit} onChange={onUnitChange} />
|
|
||||||
</div>
|
|
||||||
{showMinMax && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
label="Min"
|
|
||||||
labelWidth={labelWidth}
|
|
||||||
onChange={onMinChange}
|
|
||||||
onBlur={commitChanges}
|
|
||||||
value={min}
|
|
||||||
placeholder="Auto"
|
|
||||||
type="number"
|
|
||||||
aria-label="Field properties editor min input"
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
label="Max"
|
|
||||||
labelWidth={labelWidth}
|
|
||||||
onChange={onMaxChange}
|
|
||||||
onBlur={commitChanges}
|
|
||||||
value={max}
|
|
||||||
type="number"
|
|
||||||
placeholder="Auto"
|
|
||||||
aria-label="Field properties editor max input"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<FormField
|
|
||||||
label="Decimals"
|
|
||||||
labelWidth={labelWidth}
|
|
||||||
placeholder="auto"
|
|
||||||
onChange={onDecimalChange}
|
|
||||||
onBlur={commitChanges}
|
|
||||||
value={decimals}
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
export { FieldDisplayEditor } from './FieldDisplayEditor';
|
export { FieldDisplayEditor } from './FieldDisplayEditor';
|
||||||
export { FieldPropertiesEditor } from './FieldPropertiesEditor';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SingleStatBaseOptions,
|
SingleStatBaseOptions,
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { object } from '@storybook/addon-knobs';
|
|
||||||
|
|
||||||
import { ThresholdsEditor } from './ThresholdsEditor';
|
|
||||||
import { ThresholdsMode } from '@grafana/data';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Panel/ThresholdsEditor',
|
|
||||||
component: ThresholdsEditor,
|
|
||||||
};
|
|
||||||
|
|
||||||
const getKnobs = () => {
|
|
||||||
return {
|
|
||||||
initThresholds: object('Initial thresholds', {
|
|
||||||
mode: ThresholdsMode.Absolute,
|
|
||||||
steps: [
|
|
||||||
{ value: -Infinity, color: 'green' },
|
|
||||||
{ value: 50, color: 'red' },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const basic = () => <ThresholdsEditor onChange={action('Thresholds changed')} />;
|
|
||||||
|
|
||||||
export const withThresholds = () => (
|
|
||||||
<ThresholdsEditor thresholds={getKnobs().initThresholds} onChange={action('Thresholds changed')} />
|
|
||||||
);
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import React, { ChangeEvent } from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { GrafanaThemeType, ThresholdsMode } from '@grafana/data';
|
|
||||||
import { ThresholdsEditor, Props, thresholdsWithoutKey } from './ThresholdsEditor';
|
|
||||||
import { colors } from '../../utils/colors';
|
|
||||||
import { mockThemeContext } from '../../themes/ThemeContext';
|
|
||||||
|
|
||||||
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('Render', () => {
|
|
||||||
let restoreThemeContext: any;
|
|
||||||
beforeAll(() => {
|
|
||||||
restoreThemeContext = mockThemeContext({ type: GrafanaThemeType.Dark });
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
restoreThemeContext();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render with base threshold', () => {
|
|
||||||
const { wrapper } = setup();
|
|
||||||
expect(wrapper.find('.thresholds')).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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.onAddThresholdAfter(instance.state.steps[0]);
|
|
||||||
|
|
||||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
|
||||||
{ value: -Infinity, color: 'green' }, // 0
|
|
||||||
{ value: 50, color: colors[1] }, // 1
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add another threshold above a first', () => {
|
|
||||||
const { instance } = setup({
|
|
||||||
thresholds: {
|
|
||||||
mode: ThresholdsMode.Absolute,
|
|
||||||
steps: [
|
|
||||||
{ value: -Infinity, color: colors[0] }, // 0
|
|
||||||
{ value: 50, color: colors[2] }, // 1
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
instance.onAddThresholdAfter(instance.state.steps[1]);
|
|
||||||
|
|
||||||
expect(getCurrentThresholds(instance).steps).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: {
|
|
||||||
mode: ThresholdsMode.Absolute,
|
|
||||||
steps: [
|
|
||||||
{ value: -Infinity, color: colors[0] },
|
|
||||||
{ value: 50, color: colors[2] },
|
|
||||||
{ value: 75, color: colors[3] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
instance.onAddThresholdAfter(instance.state.steps[1]);
|
|
||||||
|
|
||||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
|
||||||
{ value: -Infinity, color: colors[0] },
|
|
||||||
{ value: 50, color: colors[2] },
|
|
||||||
{ value: 62.5, color: colors[4] },
|
|
||||||
{ value: 75, color: colors[3] },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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: 78, color: '#EAB839' },
|
|
||||||
{ value: 75, color: '#6ED0E0' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
import React, { PureComponent, ChangeEvent } from 'react';
|
|
||||||
import { Threshold, sortThresholds, ThresholdsConfig, ThresholdsMode, SelectableValue } from '@grafana/data';
|
|
||||||
import { colors } from '../../utils';
|
|
||||||
import { getColorFromHexRgbOrName } from '@grafana/data';
|
|
||||||
import { ThemeContext } from '../../themes/ThemeContext';
|
|
||||||
import { Input } from '../Forms/Legacy/Input/Input';
|
|
||||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
|
||||||
import { css } from 'emotion';
|
|
||||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
thresholds?: ThresholdsConfig;
|
|
||||||
onChange: (thresholds: ThresholdsConfig) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
steps: ThresholdWithKey[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ThresholdWithKey extends Threshold {
|
|
||||||
key: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
let counter = 100;
|
|
||||||
|
|
||||||
export class ThresholdsEditor extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const steps = toThresholdsWithKey(props.thresholds);
|
|
||||||
steps[0].value = -Infinity;
|
|
||||||
|
|
||||||
this.state = { steps };
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddThresholdAfter = (threshold: ThresholdWithKey) => {
|
|
||||||
const { steps } = this.state;
|
|
||||||
|
|
||||||
const maxValue = 100;
|
|
||||||
const minValue = 0;
|
|
||||||
|
|
||||||
let prev: ThresholdWithKey | undefined = undefined;
|
|
||||||
let next: ThresholdWithKey | undefined = undefined;
|
|
||||||
for (const t of steps) {
|
|
||||||
if (prev && prev.key === threshold.key) {
|
|
||||||
next = t;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
prev = t;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevValue = prev && isFinite(prev.value) ? prev.value : minValue;
|
|
||||||
const nextValue = next && isFinite(next.value) ? next.value : maxValue;
|
|
||||||
|
|
||||||
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 = [...steps, add];
|
|
||||||
sortThresholds(newThresholds);
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
steps: newThresholds,
|
|
||||||
},
|
|
||||||
() => 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;
|
|
||||||
}
|
|
||||||
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 = (item: SelectableValue<ThresholdsMode>) => {
|
|
||||||
if (item.value) {
|
|
||||||
this.props.onChange({
|
|
||||||
...getThresholdOrDefault(this.props.thresholds),
|
|
||||||
mode: item.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderInput = (threshold: ThresholdWithKey) => {
|
|
||||||
const config = getThresholdOrDefault(this.props.thresholds);
|
|
||||||
const isPercent = config.mode === ThresholdsMode.Percentage;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="thresholds-row-input-inner">
|
|
||||||
<span className="thresholds-row-input-inner-arrow" />
|
|
||||||
<div className="thresholds-row-input-inner-color">
|
|
||||||
{threshold.color && (
|
|
||||||
<div className="thresholds-row-input-inner-color-colorpicker">
|
|
||||||
<ColorPicker
|
|
||||||
color={threshold.color}
|
|
||||||
onChange={color => this.onChangeThresholdColor(threshold, color)}
|
|
||||||
enableNamedColors={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!isFinite(threshold.value) ? (
|
|
||||||
<div className="thresholds-row-input-inner-value">
|
|
||||||
<Input type="text" value="Base" readOnly />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="thresholds-row-input-inner-value">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.0001"
|
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => this.onChangeThresholdValue(event, threshold)}
|
|
||||||
value={threshold.value}
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { steps } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PanelOptionsGroup title="Thresholds">
|
|
||||||
<ThemeContext.Consumer>
|
|
||||||
{theme => (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ThemeContext.Consumer>
|
|
||||||
</PanelOptionsGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function thresholdsWithoutKey(
|
|
||||||
thresholds: ThresholdsConfig | undefined,
|
|
||||||
steps: ThresholdWithKey[]
|
|
||||||
): ThresholdsConfig {
|
|
||||||
thresholds = getThresholdOrDefault(thresholds);
|
|
||||||
|
|
||||||
const mode = thresholds.mode ?? ThresholdsMode.Absolute;
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
steps: steps.map(t => {
|
|
||||||
const { key, ...rest } = t;
|
|
||||||
return rest; // everything except key
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThresholdOrDefault(thresholds?: ThresholdsConfig): ThresholdsConfig {
|
|
||||||
return thresholds ?? { steps: [], mode: ThresholdsMode.Absolute };
|
|
||||||
}
|
|
||||||
|
|
||||||
function toThresholdsWithKey(thresholds?: ThresholdsConfig): ThresholdWithKey[] {
|
|
||||||
thresholds = getThresholdOrDefault(thresholds);
|
|
||||||
|
|
||||||
let steps: Threshold[] = thresholds.steps || [];
|
|
||||||
|
|
||||||
if (thresholds.steps && thresholds.steps.length === 0) {
|
|
||||||
steps = [{ value: -Infinity, color: 'green' }];
|
|
||||||
}
|
|
||||||
|
|
||||||
return steps.map(t => {
|
|
||||||
return {
|
|
||||||
color: t.color,
|
|
||||||
value: t.value === null ? -Infinity : t.value,
|
|
||||||
key: counter++,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
.thresholds {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 62px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row:first-child > .thresholds-row-color-indicator {
|
|
||||||
border-top-left-radius: $border-radius;
|
|
||||||
border-top-right-radius: $border-radius;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row:last-child > .thresholds-row-color-indicator {
|
|
||||||
border-bottom-left-radius: $border-radius;
|
|
||||||
border-bottom-right-radius: $border-radius;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-add-button {
|
|
||||||
@include buttonBackground($btn-success-bg, $btn-success-bg-hl, #fff);
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
margin-right: 5px;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-color-indicator {
|
|
||||||
width: 10px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-input {
|
|
||||||
margin-top: 44px;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-input-inner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-input-inner > *:last-child {
|
|
||||||
border-top-right-radius: $border-radius;
|
|
||||||
border-bottom-right-radius: $border-radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-input-inner-arrow {
|
|
||||||
align-self: center;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 6px solid transparent;
|
|
||||||
border-bottom: 6px solid transparent;
|
|
||||||
border-right: 6px solid $input-label-border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-input-inner-value > input {
|
|
||||||
height: $input-height;
|
|
||||||
padding: $input-padding;
|
|
||||||
width: 150px;
|
|
||||||
border-top: 1px solid $input-label-border-color;
|
|
||||||
border-bottom: 1px solid $input-label-border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-input-inner-color {
|
|
||||||
width: 42px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: $input-bg;
|
|
||||||
border: 1px solid $input-label-border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-input-inner-color-colorpicker {
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-row-input-inner-remove {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: $input-height;
|
|
||||||
padding: $input-padding;
|
|
||||||
width: 42px;
|
|
||||||
background-color: $input-label-bg;
|
|
||||||
border: 1px solid $input-label-border-color;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Render should render with base threshold 1`] = `
|
|
||||||
<div
|
|
||||||
className="thresholds"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="thresholds-row"
|
|
||||||
key="100"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
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="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)
|
|
||||||
color="green"
|
|
||||||
enableNamedColors={true}
|
|
||||||
onChange={[Function]}
|
|
||||||
>
|
|
||||||
<ColorPicker
|
|
||||||
color="green"
|
|
||||||
enableNamedColors={true}
|
|
||||||
onChange={[Function]}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import React, { ChangeEvent, PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormField } from '../FormField/FormField';
|
|
||||||
import { FormLabel } from '../FormLabel/FormLabel';
|
|
||||||
import { Input } from '../Forms/Legacy/Input/Input';
|
|
||||||
import { Select } from '../Forms/Legacy/Select/Select';
|
|
||||||
|
|
||||||
import { MappingType, ValueMapping } from '@grafana/data';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
valueMapping: ValueMapping;
|
|
||||||
updateValueMapping: (valueMapping: ValueMapping) => void;
|
|
||||||
removeValueMapping: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
from?: string;
|
|
||||||
id: number;
|
|
||||||
operator: string;
|
|
||||||
text: string;
|
|
||||||
to?: string;
|
|
||||||
type: MappingType;
|
|
||||||
value?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mappingOptions = [
|
|
||||||
{ value: MappingType.ValueToText, label: 'Value' },
|
|
||||||
{ value: MappingType.RangeToText, label: 'Range' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default class LegacyMappingRow extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = { ...props.valueMapping };
|
|
||||||
}
|
|
||||||
|
|
||||||
onMappingValueChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.setState({ value: event.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMappingFromChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.setState({ from: event.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMappingToChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.setState({ to: event.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMappingTextChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.setState({ text: event.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMappingTypeChange = (mappingType: MappingType) => {
|
|
||||||
this.setState({ type: mappingType });
|
|
||||||
};
|
|
||||||
|
|
||||||
updateMapping = () => {
|
|
||||||
this.props.updateValueMapping({ ...this.state } as ValueMapping);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderRow() {
|
|
||||||
const { from, text, to, type, value } = this.state;
|
|
||||||
|
|
||||||
if (type === MappingType.RangeToText) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
label="From"
|
|
||||||
labelWidth={4}
|
|
||||||
inputWidth={8}
|
|
||||||
onBlur={this.updateMapping}
|
|
||||||
onChange={this.onMappingFromChange}
|
|
||||||
value={from}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
label="To"
|
|
||||||
labelWidth={4}
|
|
||||||
inputWidth={8}
|
|
||||||
onBlur={this.updateMapping}
|
|
||||||
onChange={this.onMappingToChange}
|
|
||||||
value={to}
|
|
||||||
/>
|
|
||||||
<div className="gf-form gf-form--grow">
|
|
||||||
<FormLabel width={4}>Text</FormLabel>
|
|
||||||
<Input
|
|
||||||
className="gf-form-input"
|
|
||||||
onBlur={this.updateMapping}
|
|
||||||
value={text}
|
|
||||||
onChange={this.onMappingTextChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
label="Value"
|
|
||||||
labelWidth={4}
|
|
||||||
onBlur={this.updateMapping}
|
|
||||||
onChange={this.onMappingValueChange}
|
|
||||||
value={value}
|
|
||||||
inputWidth={8}
|
|
||||||
/>
|
|
||||||
<div className="gf-form gf-form--grow">
|
|
||||||
<FormLabel width={4}>Text</FormLabel>
|
|
||||||
<Input
|
|
||||||
className="gf-form-input"
|
|
||||||
onBlur={this.updateMapping}
|
|
||||||
value={text}
|
|
||||||
onChange={this.onMappingTextChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { type } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="gf-form-inline">
|
|
||||||
<div className="gf-form">
|
|
||||||
<FormLabel width={5}>Type</FormLabel>
|
|
||||||
<Select
|
|
||||||
placeholder="Choose type"
|
|
||||||
isSearchable={false}
|
|
||||||
options={mappingOptions}
|
|
||||||
value={mappingOptions.find(o => o.value === type)}
|
|
||||||
// @ts-ignore
|
|
||||||
onChange={type => this.onMappingTypeChange(type.value)}
|
|
||||||
width={7}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{this.renderRow()}
|
|
||||||
<div className="gf-form">
|
|
||||||
<button onClick={this.props.removeValueMapping} className="gf-form-label gf-form-label--btn">
|
|
||||||
<i className="fa fa-times" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { storiesOf } from '@storybook/react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { LegacyValueMappingsEditor } from './LegacyValueMappingsEditor';
|
|
||||||
|
|
||||||
const ValueMappingsEditorStories = storiesOf('Panel/LegacyValueMappingsEditor', module);
|
|
||||||
|
|
||||||
ValueMappingsEditorStories.add('default', () => {
|
|
||||||
return <LegacyValueMappingsEditor valueMappings={[]} onChange={action('Mapping changed')} />;
|
|
||||||
});
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import { LegacyValueMappingsEditor, Props } from './LegacyValueMappingsEditor';
|
|
||||||
import { MappingType } from '@grafana/data';
|
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
|
||||||
const props: Props = {
|
|
||||||
onChange: jest.fn(),
|
|
||||||
valueMappings: [
|
|
||||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
|
||||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
|
||||||
|
|
||||||
const wrapper = shallow(<LegacyValueMappingsEditor {...props} />);
|
|
||||||
|
|
||||||
const instance = wrapper.instance() as LegacyValueMappingsEditor;
|
|
||||||
|
|
||||||
return {
|
|
||||||
instance,
|
|
||||||
wrapper,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Render', () => {
|
|
||||||
it('should render component', () => {
|
|
||||||
const { wrapper } = setup();
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('On remove mapping', () => {
|
|
||||||
it('Should remove mapping with id 0', () => {
|
|
||||||
const { instance } = setup();
|
|
||||||
|
|
||||||
instance.onRemoveMapping(1);
|
|
||||||
|
|
||||||
expect(instance.state.valueMappings).toEqual([
|
|
||||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove mapping with id 1', () => {
|
|
||||||
const { instance } = setup();
|
|
||||||
|
|
||||||
instance.onRemoveMapping(2);
|
|
||||||
|
|
||||||
expect(instance.state.valueMappings).toEqual([
|
|
||||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Next id to add', () => {
|
|
||||||
it('should be 4', () => {
|
|
||||||
const { instance } = setup();
|
|
||||||
|
|
||||||
instance.onAddMapping();
|
|
||||||
|
|
||||||
expect(instance.state.nextIdToAdd).toEqual(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should default to 1', () => {
|
|
||||||
const { instance } = setup({ valueMappings: [] });
|
|
||||||
|
|
||||||
expect(instance.state.nextIdToAdd).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import LegacyMappingRow from './LegacyMappingRow';
|
|
||||||
import { MappingType, ValueMapping } from '@grafana/data';
|
|
||||||
import { Button } from '../Button';
|
|
||||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
valueMappings?: ValueMapping[];
|
|
||||||
onChange: (valueMappings: ValueMapping[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
valueMappings: ValueMapping[];
|
|
||||||
nextIdToAdd: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LegacyValueMappingsEditor extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const mappings = props.valueMappings || [];
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
valueMappings: mappings,
|
|
||||||
nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromValueMappings(mappings) : 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaxIdFromValueMappings(mappings: ValueMapping[]) {
|
|
||||||
return (
|
|
||||||
Math.max.apply(
|
|
||||||
null,
|
|
||||||
mappings.map(mapping => mapping.id).map(m => m)
|
|
||||||
) + 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddMapping = () =>
|
|
||||||
this.setState(prevState => ({
|
|
||||||
valueMappings: [
|
|
||||||
...prevState.valueMappings,
|
|
||||||
{
|
|
||||||
id: prevState.nextIdToAdd,
|
|
||||||
operator: '',
|
|
||||||
value: '',
|
|
||||||
text: '',
|
|
||||||
type: MappingType.ValueToText,
|
|
||||||
from: '',
|
|
||||||
to: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
nextIdToAdd: prevState.nextIdToAdd + 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
onRemoveMapping = (id: number) => {
|
|
||||||
this.setState(
|
|
||||||
prevState => ({
|
|
||||||
valueMappings: prevState.valueMappings.filter(m => {
|
|
||||||
return m.id !== id;
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
() => {
|
|
||||||
this.props.onChange(this.state.valueMappings);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateGauge = (mapping: ValueMapping) => {
|
|
||||||
this.setState(
|
|
||||||
prevState => ({
|
|
||||||
valueMappings: prevState.valueMappings.map(m => {
|
|
||||||
if (m.id === mapping.id) {
|
|
||||||
return { ...mapping };
|
|
||||||
}
|
|
||||||
|
|
||||||
return m;
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
() => {
|
|
||||||
this.props.onChange(this.state.valueMappings);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { valueMappings } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PanelOptionsGroup title="Value mappings">
|
|
||||||
<div>
|
|
||||||
{valueMappings.length > 0 &&
|
|
||||||
valueMappings.map((valueMapping, index) => (
|
|
||||||
<LegacyMappingRow
|
|
||||||
key={`${valueMapping.text}-${index}`}
|
|
||||||
valueMapping={valueMapping}
|
|
||||||
updateValueMapping={this.updateGauge}
|
|
||||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Button variant="primary" icon="plus-circle" onClick={this.onAddMapping}>
|
|
||||||
Add mapping
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PanelOptionsGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
.mapping-row {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-mapping-row {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 37px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: $border-radius;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-mapping-row-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
background-color: $green-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-mapping-row-label {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
padding: 5px 8px;
|
|
||||||
background-color: $input-label-bg;
|
|
||||||
width: calc(100% - 36px);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
|
||||||
<Component
|
|
||||||
title="Value mappings"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<LegacyMappingRow
|
|
||||||
key="Ok-0"
|
|
||||||
removeValueMapping={[Function]}
|
|
||||||
updateValueMapping={[Function]}
|
|
||||||
valueMapping={
|
|
||||||
Object {
|
|
||||||
"id": 1,
|
|
||||||
"operator": "",
|
|
||||||
"text": "Ok",
|
|
||||||
"type": 1,
|
|
||||||
"value": "20",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<LegacyMappingRow
|
|
||||||
key="Meh-1"
|
|
||||||
removeValueMapping={[Function]}
|
|
||||||
updateValueMapping={[Function]}
|
|
||||||
valueMapping={
|
|
||||||
Object {
|
|
||||||
"from": "21",
|
|
||||||
"id": 2,
|
|
||||||
"operator": "",
|
|
||||||
"text": "Meh",
|
|
||||||
"to": "30",
|
|
||||||
"type": 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon="plus-circle"
|
|
||||||
onClick={[Function]}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Add mapping
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Component>
|
|
||||||
`;
|
|
||||||
@@ -10,9 +10,7 @@
|
|||||||
@import 'RefreshPicker/RefreshPicker';
|
@import 'RefreshPicker/RefreshPicker';
|
||||||
@import 'Forms/Legacy/Select/Select';
|
@import 'Forms/Legacy/Select/Select';
|
||||||
@import 'TableInputCSV/TableInputCSV';
|
@import 'TableInputCSV/TableInputCSV';
|
||||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
|
||||||
@import 'TimePicker/TimeOfDayPicker';
|
@import 'TimePicker/TimeOfDayPicker';
|
||||||
@import 'Tooltip/Tooltip';
|
@import 'Tooltip/Tooltip';
|
||||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
|
||||||
@import 'Alert/Alert';
|
@import 'Alert/Alert';
|
||||||
@import 'Slider/Slider';
|
@import 'Slider/Slider';
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
|
|||||||
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
|
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
|
||||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||||
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||||
export { LegacyValueMappingsEditor } from './ValueMappingsEditor/LegacyValueMappingsEditor';
|
|
||||||
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
||||||
export { PieChart, PieChartType } from './PieChart/PieChart';
|
export { PieChart, PieChartType } from './PieChart/PieChart';
|
||||||
export { UnitPicker } from './UnitPicker/UnitPicker';
|
export { UnitPicker } from './UnitPicker/UnitPicker';
|
||||||
@@ -92,7 +91,6 @@ export { getLogRowStyles } from './Logs/getLogRowStyles';
|
|||||||
export { ToggleButtonGroup, ToggleButton } from './ToggleButtonGroup/ToggleButtonGroup';
|
export { ToggleButtonGroup, ToggleButton } from './ToggleButtonGroup/ToggleButtonGroup';
|
||||||
// Panel editors
|
// Panel editors
|
||||||
export { FullWidthButtonContainer } from './Button/FullWidthButtonContainer';
|
export { FullWidthButtonContainer } from './Button/FullWidthButtonContainer';
|
||||||
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
|
|
||||||
export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
|
export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
|
||||||
export * from './SingleStatShared/index';
|
export * from './SingleStatShared/index';
|
||||||
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
||||||
|
|||||||
Reference in New Issue
Block a user