mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into 14812/formgroup-component
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="gf-form"
|
||||
className="form-field"
|
||||
>
|
||||
<Component
|
||||
width={11}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { SFC, ReactNode } from 'react';
|
||||
import { Tooltip } from '..';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
|
||||
interface Props {
|
||||
tooltip?: string;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -6,4 +6,5 @@
|
||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||
@import 'ColorPicker/ColorPicker';
|
||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||
@import "FormField/FormField";
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './series';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
export * from './gauge';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user