Merge branch 'master' into 14812/formgroup-component

This commit is contained in:
Peter Holmberg
2019-01-16 14:27:47 +00:00
54 changed files with 2938 additions and 1561 deletions

View File

@@ -6,6 +6,7 @@ interface Props {
autoHide?: boolean;
autoHideTimeout?: number;
autoHideDuration?: number;
autoMaxHeight?: string;
hideTracksWhenNotNeeded?: boolean;
}
@@ -18,11 +19,12 @@ export class CustomScrollbar extends PureComponent<Props> {
autoHide: true,
autoHideTimeout: 200,
autoHideDuration: 200,
autoMaxHeight: '100%',
hideTracksWhenNotNeeded: false,
};
render() {
const { customClassName, children, ...scrollProps } = this.props;
const { customClassName, children, autoMaxHeight } = this.props;
return (
<Scrollbars
@@ -31,13 +33,12 @@ export class CustomScrollbar extends PureComponent<Props> {
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
// Before these where set to inhert but that caused problems with cut of legends in firefox
autoHeightMin={'0'}
autoHeightMax={'100%'}
autoHeightMax={autoMaxHeight}
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
renderTrackVertical={props => <div {...props} className="track-vertical" />}
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
renderView={props => <div {...props} className="view" />}
{...scrollProps}
>
{children}
</Scrollbars>

View File

@@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
Object {
"display": "none",
"height": 6,
"opacity": 0,
"position": "absolute",
"transition": "opacity 200ms",
}
}
>
@@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
style={
Object {
"display": "none",
"opacity": 0,
"position": "absolute",
"transition": "opacity 200ms",
"width": 6,
}
}

View File

@@ -2,7 +2,7 @@
exports[`Render should render component 1`] = `
<div
className="gf-form"
className="form-field"
>
<Component
width={11}

View File

@@ -1,5 +1,5 @@
import React, { SFC, ReactNode } from 'react';
import { Tooltip } from '..';
import { Tooltip } from '../Tooltip/Tooltip';
interface Props {
tooltip?: string;

View File

@@ -16,7 +16,7 @@ import SelectOptionGroup from './SelectOptionGroup';
import IndicatorsContainer from './IndicatorsContainer';
import NoOptionsMessage from './NoOptionsMessage';
import resetSelectStyles from './resetSelectStyles';
import { CustomScrollbar } from '@grafana/ui';
import { CustomScrollbar } from '..';
export interface SelectOptionItem {
label?: string;
@@ -61,7 +61,7 @@ interface AsyncProps {
export const MenuList = (props: any) => {
return (
<components.MenuList {...props}>
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
<CustomScrollbar autoHide={false} autoMaxHeight="inherit">{props.children}</CustomScrollbar>
</components.MenuList>
);
};

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { shallow } from 'enzyme';
import { ThresholdsEditor, Props } from './ThresholdsEditor';
import { BasicGaugeColor } from '../../types';
const setup = (propOverrides?: object) => {
const props: Props = {
@@ -15,49 +14,160 @@ const setup = (propOverrides?: object) => {
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
};
describe('Initialization', () => {
it('should add a base threshold if missing', () => {
const instance = setup();
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
});
});
describe('Add threshold', () => {
it('should add threshold', () => {
it('should not add threshold at index 0', () => {
const instance = setup();
instance.onAddThreshold(0);
expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
});
it('should add another threshold above a first', () => {
const instance = setup({
thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
});
it('should add threshold', () => {
const instance = setup();
instance.onAddThreshold(1);
expect(instance.state.thresholds).toEqual([
{ index: 1, value: 75, color: 'rgb(170, 95, 61)' },
{ index: 0, value: 50, color: 'rgb(127, 115, 64)' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
]);
});
it('should add another threshold above a first', () => {
const instance = setup({
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
});
instance.onAddThreshold(2);
expect(instance.state.thresholds).toEqual([
{ index: 2, value: 75, color: '#6ED0E0' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
]);
});
it('should add another threshold between first and second index', () => {
const instance = setup({
thresholds: [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
],
});
instance.onAddThreshold(2);
expect(instance.state.thresholds).toEqual([
{ index: 3, value: 75, color: '#6ED0E0' },
{ index: 2, value: 62.5, color: '#EF843C' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
]);
});
});
describe('Remove threshold', () => {
it('should not remove threshold at index 0', () => {
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
const instance = setup({ thresholds });
instance.onRemoveThreshold(thresholds[0]);
expect(instance.state.thresholds).toEqual(thresholds);
});
it('should remove threshold', () => {
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
const instance = setup({
thresholds,
});
instance.onRemoveThreshold(thresholds[1]);
expect(instance.state.thresholds).toEqual([
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 75, color: '#6ED0E0' },
]);
});
});
describe('change threshold value', () => {
it('should update value and resort rows', () => {
it('should not change threshold at index 0', () => {
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
const instance = setup({ thresholds });
const mockEvent = { target: { value: 12 } };
instance.onChangeThresholdValue(mockEvent, thresholds[0]);
expect(instance.state.thresholds).toEqual(thresholds);
});
it('should update value', () => {
const instance = setup();
const mockThresholds = [
{ index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
instance.state = {
baseColor: BasicGaugeColor.Green,
thresholds: mockThresholds,
thresholds,
};
const mockEvent = { target: { value: 78 } };
instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
expect(instance.state.thresholds).toEqual([
{ index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 78, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
]);
});
});
describe('on blur threshold value', () => {
it('should resort rows and update indexes', () => {
const instance = setup();
const thresholds = [
{ index: 0, value: -Infinity, color: '#7EB26D' },
{ index: 1, value: 78, color: '#EAB839' },
{ index: 2, value: 75, color: '#6ED0E0' },
];
instance.state = {
thresholds,
};
instance.onBlur();
expect(instance.state.thresholds).toEqual([
{ index: 2, value: 78, color: '#EAB839' },
{ index: 1, value: 75, color: '#6ED0E0' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
]);
});
});

View File

@@ -1,9 +1,10 @@
import React, { PureComponent } from 'react';
import tinycolor, { ColorInput } from 'tinycolor2';
// import tinycolor, { ColorInput } from 'tinycolor2';
import { Threshold, BasicGaugeColor } from '../../types';
import { Threshold } from '../../types';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
import { colors } from '../../utils';
export interface Props {
thresholds: Threshold[];
@@ -12,50 +13,43 @@ export interface Props {
interface State {
thresholds: Threshold[];
baseColor: string;
}
export class ThresholdsEditor extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green };
const thresholds: Threshold[] =
props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
this.state = { thresholds };
}
onAddThreshold = (index: number) => {
const maxValue = 100; // hardcoded for now before we add the base threshold
const minValue = 0; // hardcoded for now before we add the base threshold
const { thresholds } = this.state;
const maxValue = 100;
const minValue = 0;
if (index === 0) {
return;
}
const newThresholds = thresholds.map(threshold => {
if (threshold.index >= index) {
threshold = {
...threshold,
index: threshold.index + 1,
};
const index = threshold.index + 1;
threshold = { ...threshold, index };
}
return threshold;
});
// Setting value to a value between the previous thresholds
let value;
const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0];
const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0];
const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue;
const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue;
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
if (index === 0 && thresholds.length === 0) {
value = maxValue - (maxValue - minValue) / 2;
} else if (index === 0 && thresholds.length > 0) {
value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2;
} else if (index > newThresholds[newThresholds.length - 1].index) {
value = maxValue - (maxValue - newThresholds[index - 1].value) / 2;
}
// Set a color that lies between the previous thresholds
let color;
if (index === 0 && thresholds.length === 0) {
color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
} else {
color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString();
}
// Set a color
const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0];
this.setState(
{
@@ -73,18 +67,40 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
};
onRemoveThreshold = (threshold: Threshold) => {
if (threshold.index === 0) {
return;
}
this.setState(
prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
prevState => {
const newThresholds = prevState.thresholds.map(t => {
if (t.index > threshold.index) {
const index = t.index - 1;
t = { ...t, index };
}
return t;
});
return {
thresholds: newThresholds.filter(t => t !== threshold),
};
},
() => this.updateGauge()
);
};
onChangeThresholdValue = (event: any, threshold: Threshold) => {
if (threshold.index === 0) {
return;
}
const { thresholds } = this.state;
const parsedValue = parseInt(event.target.value, 10);
const value = isNaN(parsedValue) ? null : parsedValue;
const newThresholds = thresholds.map(t => {
if (t === threshold) {
t = { ...t, value: event.target.value };
t = { ...t, value: value as number };
}
return t;
@@ -114,7 +130,14 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
onBlur = () => {
this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
this.setState(prevState => {
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
let index = sortThresholds.length - 1;
sortThresholds.forEach(t => {
t.index = index--;
});
return { thresholds: sortThresholds };
});
this.updateGauge();
};
@@ -129,92 +152,53 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
});
};
renderThresholds() {
const { thresholds } = this.state;
return thresholds.map((threshold, index) => {
return (
<div className="threshold-row" key={`${threshold.index}-${index}`}>
<div className="threshold-row-inner">
<div className="threshold-row-color">
{threshold.color && (
<div className="threshold-row-color-inner">
<ColorPicker
color={threshold.color}
onChange={color => this.onChangeThresholdColor(threshold, color)}
/>
</div>
)}
</div>
<input
className="threshold-row-input"
type="text"
onChange={event => this.onChangeThresholdValue(event, threshold)}
value={threshold.value}
onBlur={this.onBlur}
/>
<div onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
<i className="fa fa-times" />
</div>
</div>
</div>
);
});
}
renderIndicator() {
const { thresholds } = this.state;
return thresholds.map((t, i) => {
return (
<div key={`${t.value}-${i}`} className="indicator-section">
<div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
<div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
</div>
);
});
}
renderBaseIndicator() {
renderInput = (threshold: Threshold) => {
const value = threshold.index === 0 ? 'Base' : threshold.value;
return (
<div className="indicator-section" style={{ height: '100%' }}>
<div
onClick={() => this.onAddThreshold(0)}
style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
/>
<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)} />
</div>
)}
</div>
<div className="thresholds-row-input-inner-value">
<input
type="text"
onChange={event => this.onChangeThresholdValue(event, threshold)}
value={value}
onBlur={this.onBlur}
readOnly={threshold.index === 0}
/>
</div>
{threshold.index > 0 && (
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
<i className="fa fa-times" />
</div>
)}
</div>
);
}
renderBase() {
const baseColor = BasicGaugeColor.Green;
return (
<div className="threshold-row threshold-row-base">
<div className="threshold-row-inner threshold-row-inner--base">
<div className="threshold-row-color">
<div className="threshold-row-color-inner">
<ColorPicker color={baseColor} onChange={color => this.onChangeBaseColor(color)} />
</div>
</div>
<div className="threshold-row-label">Base</div>
</div>
</div>
);
}
};
render() {
const { thresholds } = this.state;
return (
<PanelOptionsGroup title="Thresholds">
<div className="thresholds">
<div className="color-indicators">
{this.renderIndicator()}
{this.renderBaseIndicator()}
</div>
<div className="threshold-rows">
{this.renderThresholds()}
{this.renderBase()}
</div>
{thresholds.map((threshold, index) => {
return (
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
<div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
<i className="fa fa-plus" />
</div>
<div className="thresholds-row-color-indicator" style={{ backgroundColor: threshold.color }} />
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
);
})}
</div>
</PanelOptionsGroup>
);

View File

@@ -1,46 +1,99 @@
.thresholds {
margin-bottom: 10px;
}
.thresholds-row {
display: flex;
flex-direction: row;
height: 70px;
}
.threshold-rows {
margin-left: 5px;
.thresholds-row:first-child > .thresholds-row-color-indicator {
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
overflow: hidden;
}
.threshold-row {
.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 {
align-self: center;
margin-right: 5px;
color: $green;
height: 24px;
width: 24px;
background-color: $green;
border-radius: 50%;
display: flex;
align-items: center;
margin-top: 3px;
padding: 5px;
&::before {
font-family: 'FontAwesome';
content: '\f0d9';
color: $input-label-border-color;
}
justify-content: center;
cursor: pointer;
}
.threshold-row-inner {
border: 1px solid $input-label-border-color;
border-radius: $border-radius;
.thresholds-row-add-button > i {
color: $white;
}
.thresholds-row-color-indicator {
width: 10px;
}
.thresholds-row-input {
margin-top: 49px;
margin-left: 2px;
}
.thresholds-row-input-inner {
display: flex;
overflow: hidden;
height: 37px;
&--base {
width: auto;
}
justify-content: center;
flex-direction: row;
height: 42px;
}
.threshold-row-color {
width: 36px;
border-right: 1px solid $input-label-border-color;
.thresholds-row-input-inner > div {
border-left: 1px solid $input-label-border-color;
border-top: 1px solid $input-label-border-color;
border-bottom: 1px solid $input-label-border-color;
}
.thresholds-row-input-inner > *:nth-child(2) {
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
.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: 100%;
padding: 8px 10px;
width: 150px;
}
.thresholds-row-input-inner-color {
width: 42px;
display: flex;
align-items: center;
justify-content: center;
background-color: $input-bg;
}
.threshold-row-color-inner {
.thresholds-row-input-inner-color-colorpicker {
border-radius: 10px;
overflow: hidden;
display: flex;
@@ -48,56 +101,12 @@
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}
.threshold-row-input {
padding: 8px 10px;
width: 150px;
}
.threshold-row-label {
background-color: $input-label-bg;
padding: 5px;
display: flex;
align-items: center;
}
.threshold-row-add-label {
align-items: center;
display: flex;
padding: 5px 8px;
}
.threshold-row-remove {
.thresholds-row-input-inner-remove {
display: flex;
align-items: center;
justify-content: center;
height: 37px;
width: 37px;
height: 42px;
width: 42px;
background-color: $input-label-border-color;
cursor: pointer;
}
.threshold-row-add {
border-right: $border-width solid $input-label-border-color;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
background-color: $green;
}
.threshold-row-label {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.indicator-section {
width: 100%;
height: 50px;
cursor: pointer;
}
.color-indicators {
width: 15px;
border-bottom-left-radius: $border-radius;
border-bottom-right-radius: $border-radius;
overflow: hidden;
}

View File

@@ -0,0 +1,147 @@
import React, { ChangeEvent, PureComponent } from 'react';
import { MappingType, ValueMapping } from '../../types';
import { FormField, Label, Select } from '..';
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 MappingRow 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}
inputProps={{
onChange: (event: ChangeEvent<HTMLInputElement>) => this.onMappingFromChange(event),
onBlur: () => this.updateMapping(),
value: from,
}}
inputWidth={8}
/>
<FormField
label="To"
labelWidth={4}
inputProps={{
onBlur: () => this.updateMapping,
onChange: (event: ChangeEvent<HTMLInputElement>) => this.onMappingToChange(event),
value: to,
}}
inputWidth={8}
/>
<div className="gf-form gf-form--grow">
<Label width={4}>Text</Label>
<input
className="gf-form-input"
onBlur={this.updateMapping}
value={text}
onChange={this.onMappingTextChange}
/>
</div>
</>
);
}
return (
<>
<FormField
label="Value"
labelWidth={4}
inputProps={{
onBlur: () => this.updateMapping,
onChange: (event: ChangeEvent<HTMLInputElement>) => this.onMappingValueChange(event),
value: value,
}}
inputWidth={8}
/>
<div className="gf-form gf-form--grow">
<Label width={4}>Text</Label>
<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">
<Label width={5}>Type</Label>
<Select
placeholder="Choose type"
isSearchable={false}
options={mappingOptions}
value={mappingOptions.find(o => o.value === type)}
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>
);
}
}

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { shallow } from 'enzyme';
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
import { MappingType } from '../../types/panel';
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(<ValueMappingsEditor {...props} />);
const instance = wrapper.instance() as ValueMappingsEditor;
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.addMapping();
expect(instance.state.nextIdToAdd).toEqual(4);
});
it('should default to 1', () => {
const { instance } = setup({ valueMappings: [] });
expect(instance.state.nextIdToAdd).toEqual(1);
});
});

View File

@@ -0,0 +1,105 @@
import React, { PureComponent } from 'react';
import MappingRow from './MappingRow';
import { MappingType, ValueMapping } from '../../types/panel';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
export interface Props {
valueMappings: ValueMapping[];
onChange: (valueMappings: ValueMapping[]) => void;
}
interface State {
valueMappings: ValueMapping[];
nextIdToAdd: number;
}
export class ValueMappingsEditor 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;
}
addMapping = () =>
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) => (
<MappingRow
key={`${valueMapping.text}-${index}`}
valueMapping={valueMapping}
updateValueMapping={this.updateGauge}
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
/>
))}
</div>
<div className="add-mapping-row" onClick={this.addMapping}>
<div className="add-mapping-row-icon">
<i className="fa fa-plus" />
</div>
<div className="add-mapping-row-label">Add mapping</div>
</div>
</PanelOptionsGroup>
);
}
}

View File

@@ -0,0 +1,29 @@
.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;
}
.add-mapping-row-label {
align-items: center;
display: flex;
padding: 5px 8px;
background-color: $input-label-bg;
width: calc(100% - 36px);
}

View File

@@ -0,0 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<Component
title="Value Mappings"
>
<div>
<MappingRow
key="Ok-0"
removeValueMapping={[Function]}
updateValueMapping={[Function]}
valueMapping={
Object {
"id": 1,
"operator": "",
"text": "Ok",
"type": 1,
"value": "20",
}
}
/>
<MappingRow
key="Meh-1"
removeValueMapping={[Function]}
updateValueMapping={[Function]}
valueMapping={
Object {
"from": "21",
"id": 2,
"operator": "",
"text": "Meh",
"to": "30",
"type": 2,
}
}
/>
</div>
<div
className="add-mapping-row"
onClick={[Function]}
>
<div
className="add-mapping-row-icon"
>
<i
className="fa fa-plus"
/>
</div>
<div
className="add-mapping-row-label"
>
Add mapping
</div>
</div>
</Component>
`;

View File

@@ -6,4 +6,5 @@
@import 'PanelOptionsGroup/PanelOptionsGroup';
@import 'PanelOptionsGrid/PanelOptionsGrid';
@import 'ColorPicker/ColorPicker';
@import 'ValueMappingsEditor/ValueMappingsEditor';
@import "FormField/FormField";

View File

@@ -22,3 +22,4 @@ export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
export { Graph } from './Graph/Graph';
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';

View File

@@ -1,16 +0,0 @@
import { RangeMap, Threshold, ValueMap } from './panel';
export interface GaugeOptions {
baseColor: string;
decimals: number;
mappings: Array<RangeMap | ValueMap>;
maxValue: number;
minValue: number;
prefix: string;
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
stat: string;
suffix: string;
thresholds: Threshold[];
unit: string;
}

View File

@@ -1,4 +1,3 @@
export * from './series';
export * from './time';
export * from './panel';
export * from './gauge';

View File

@@ -1,6 +1,8 @@
import { TimeSeries, LoadingState } from './series';
import { TimeRange } from './time';
export type InterpolateFunction = (value: string, format?: string | Function) => string;
export interface PanelProps<T = any> {
timeSeries: TimeSeries[];
timeRange: TimeRange;
@@ -9,6 +11,7 @@ export interface PanelProps<T = any> {
renderCounter: number;
width: number;
height: number;
onInterpolate: InterpolateFunction;
}
export interface PanelOptionsProps<T = any> {
@@ -53,6 +56,8 @@ interface BaseMap {
type: MappingType;
}
export type ValueMapping = ValueMap | RangeMap;
export interface ValueMap extends BaseMap {
value: string;
}