Commit 3ebba56f by Dominik Prokop Committed by GitHub

NewPanelEdit: Remove legacy editors for thresholds, value mappings and field properties (#23497)

* Remove legacy value mappings editor

* Remove FieldPropertiesEditor

* Remove legacy ThresholdsEditor
parent 94f87c72
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,
});
});
});
});
//
// 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"
/>
</>
);
};
export { FieldDisplayEditor } from './FieldDisplayEditor'; export { FieldDisplayEditor } from './FieldDisplayEditor';
export { FieldPropertiesEditor } from './FieldPropertiesEditor';
export { export {
SingleStatBaseOptions, SingleStatBaseOptions,
......
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')} />
);
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' },
]);
});
});
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++,
};
});
}
.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;
}
// 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>
`;
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>
);
}
}
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')} />;
});
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);
});
});
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>
);
}
}
.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);
}
// 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 @@ ...@@ -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'; ...@@ -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'; ...@@ -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';
......
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