mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14534 from grafana/14409/threshold-ux-changes
14409/threshold ux changes
This commit is contained in:
@@ -186,16 +186,18 @@ func getPanelSort(id string) int {
|
||||
sort = 1
|
||||
case "singlestat":
|
||||
sort = 2
|
||||
case "table":
|
||||
case "gauge":
|
||||
sort = 3
|
||||
case "text":
|
||||
case "table":
|
||||
sort = 4
|
||||
case "heatmap":
|
||||
case "text":
|
||||
sort = 5
|
||||
case "alertlist":
|
||||
case "heatmap":
|
||||
sort = 6
|
||||
case "dashlist":
|
||||
case "alertlist":
|
||||
sort = 7
|
||||
case "dashlist":
|
||||
sort = 8
|
||||
}
|
||||
return sort
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Select from './Select';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
interface Props {
|
||||
onChange: (item: any) => {} | void;
|
||||
onChange: (item: any) => void;
|
||||
defaultValue?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,46 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Switch } from 'app/core/components/Switch/Switch';
|
||||
import { OptionModuleProps } from './module';
|
||||
import { Label } from '../../../core/components/Label/Label';
|
||||
|
||||
export default class GaugeOptions extends PureComponent<OptionModuleProps> {
|
||||
toggleThresholdLabels = () =>
|
||||
onToggleThresholdLabels = () =>
|
||||
this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
|
||||
|
||||
toggleThresholdMarkers = () =>
|
||||
onToggleThresholdMarkers = () =>
|
||||
this.props.onChange({ ...this.props.options, showThresholdMarkers: !this.props.options.showThresholdMarkers });
|
||||
|
||||
onMinValueChange = ({ target }) => this.props.onChange({ ...this.props.options, minValue: target.value });
|
||||
|
||||
onMaxValueChange = ({ target }) => this.props.onChange({ ...this.props.options, maxValue: target.value });
|
||||
|
||||
render() {
|
||||
const { showThresholdLabels, showThresholdMarkers } = this.props.options;
|
||||
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers } = this.props.options;
|
||||
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Gauge</h5>
|
||||
<div className="gf-form-inline">
|
||||
<h5 className="section-heading">Gauge</h5>
|
||||
<div className="gf-form">
|
||||
<Label width={8}>Min value</Label>
|
||||
<input type="text" className="gf-form-input width-12" onChange={this.onMinValueChange} value={minValue} />
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<Label width={8}>Max value</Label>
|
||||
<input type="text" className="gf-form-input width-12" onChange={this.onMaxValueChange} value={maxValue} />
|
||||
</div>
|
||||
<Switch
|
||||
label="Threshold labels"
|
||||
labelClass="width-10"
|
||||
label="Show labels"
|
||||
labelClass="width-8"
|
||||
checked={showThresholdLabels}
|
||||
onChange={this.toggleThresholdLabels}
|
||||
onChange={this.onToggleThresholdLabels}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<Switch
|
||||
label="Threshold markers"
|
||||
labelClass="width-10"
|
||||
label="Show markers"
|
||||
labelClass="width-8"
|
||||
checked={showThresholdMarkers}
|
||||
onChange={this.toggleThresholdMarkers}
|
||||
onChange={this.onToggleThresholdMarkers}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,60 +62,51 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
|
||||
if (type === MappingType.RangeToText) {
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-inline mapping-row-input">
|
||||
<Label width={4}>From</Label>
|
||||
<div>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
className="gf-form-input width-8"
|
||||
value={from}
|
||||
onBlur={this.updateMapping}
|
||||
onChange={this.onMappingFromChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline mapping-row-input">
|
||||
<div className="gf-form">
|
||||
<Label width={4}>To</Label>
|
||||
<div>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
className="gf-form-input width-8"
|
||||
value={to}
|
||||
onBlur={this.updateMapping}
|
||||
onChange={this.onMappingToChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline mapping-row-input">
|
||||
<div className="gf-form">
|
||||
<Label width={4}>Text</Label>
|
||||
<div>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
className="gf-form-input width-10"
|
||||
value={text}
|
||||
onBlur={this.updateMapping}
|
||||
onChange={this.onMappingTextChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-inline mapping-row-input">
|
||||
<Label width={4}>Value</Label>
|
||||
<div>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
className="gf-form-input width-8"
|
||||
onBlur={this.updateMapping}
|
||||
onChange={this.onMappingValueChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline mapping-row-input">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<Label width={4}>Text</Label>
|
||||
<div>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onBlur={this.updateMapping}
|
||||
@@ -123,8 +114,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
onChange={this.onMappingTextChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,8 +122,8 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
const { type } = this.state;
|
||||
|
||||
return (
|
||||
<div className="mapping-row">
|
||||
<div className="gf-form-inline mapping-row-type">
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<Label width={5}>Type</Label>
|
||||
<Select
|
||||
placeholder="Choose type"
|
||||
@@ -144,9 +134,11 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
width={7}
|
||||
/>
|
||||
</div>
|
||||
<div>{this.renderRow()}</div>
|
||||
<div onClick={this.props.removeMapping} className="threshold-row-remove">
|
||||
{this.renderRow()}
|
||||
<div className="gf-form">
|
||||
<button onClick={this.props.removeMapping} className="gf-form-label gf-form-label--btn">
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,17 +2,14 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import Thresholds from './Thresholds';
|
||||
import { defaultProps, OptionsProps } from './module';
|
||||
import { PanelOptionsProps } from '../../../types';
|
||||
import { BasicGaugeColor, PanelOptionsProps } from 'app/types';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: PanelOptionsProps<OptionsProps> = {
|
||||
onChange: jest.fn(),
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
thresholds: [
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||
{ index: 1, label: 'Max', value: 100, canRemove: false },
|
||||
],
|
||||
thresholds: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,121 +19,51 @@ const setup = (propOverrides?: object) => {
|
||||
};
|
||||
|
||||
describe('Add threshold', () => {
|
||||
it('should add threshold between min and max', () => {
|
||||
it('should add threshold', () => {
|
||||
const instance = setup();
|
||||
|
||||
instance.onAddThreshold(1);
|
||||
instance.onAddThreshold(0);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||
{ index: 1, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 2, label: 'Max', value: 100, canRemove: false },
|
||||
]);
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
|
||||
});
|
||||
|
||||
it('should add threshold between min and added threshold', () => {
|
||||
it('should add another threshold above a first', () => {
|
||||
const instance = setup({
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
thresholds: [
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||
{ index: 1, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 2, label: 'Max', value: 100, canRemove: false },
|
||||
],
|
||||
thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
|
||||
},
|
||||
});
|
||||
|
||||
instance.onAddThreshold(1);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||
{ index: 1, label: '', value: 25, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 2, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 3, label: 'Max', value: 100, canRemove: false },
|
||||
{ index: 1, value: 75, color: 'rgb(170, 95, 61)' },
|
||||
{ index: 0, value: 50, color: 'rgb(127, 115, 64)' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add at index', () => {
|
||||
it('should return 1, no added thresholds', () => {
|
||||
const instance = setup();
|
||||
|
||||
const result = instance.insertAtIndex(1);
|
||||
|
||||
expect(result).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return 1, one added threshold', () => {
|
||||
const instance = setup();
|
||||
instance.state = {
|
||||
thresholds: [
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false },
|
||||
{ index: 1, label: '', value: 50, canRemove: true },
|
||||
{ index: 2, label: 'Max', value: 100, canRemove: false },
|
||||
],
|
||||
};
|
||||
|
||||
const result = instance.insertAtIndex(1);
|
||||
|
||||
expect(result).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return 2, two added thresholds', () => {
|
||||
const instance = setup({
|
||||
options: {
|
||||
thresholds: [
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false },
|
||||
{ index: 1, label: '', value: 25, canRemove: true },
|
||||
{ index: 2, label: '', value: 50, canRemove: true },
|
||||
{ index: 3, label: 'Max', value: 100, canRemove: false },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = instance.insertAtIndex(2);
|
||||
|
||||
expect(result).toEqual(2);
|
||||
});
|
||||
|
||||
it('should return 2, one added threshold', () => {
|
||||
const instance = setup();
|
||||
instance.state = {
|
||||
thresholds: [
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false },
|
||||
{ index: 1, label: '', value: 50, canRemove: true },
|
||||
{ index: 2, label: 'Max', value: 100, canRemove: false },
|
||||
],
|
||||
};
|
||||
|
||||
const result = instance.insertAtIndex(2);
|
||||
|
||||
expect(result).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change threshold value', () => {
|
||||
it('should update value and resort rows', () => {
|
||||
const instance = setup();
|
||||
const mockThresholds = [
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||
{ index: 1, label: '', value: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 2, label: '', value: 75, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 3, label: 'Max', value: 100, canRemove: false },
|
||||
{ index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
];
|
||||
|
||||
instance.state = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
thresholds: mockThresholds,
|
||||
};
|
||||
|
||||
const mockEvent = { target: { value: 78 } };
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, mockThresholds[1]);
|
||||
instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
|
||||
{ index: 1, label: '', value: 78, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 2, label: '', value: 75, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 3, label: 'Max', value: 100, canRemove: false },
|
||||
{ index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
||||
import { OptionModuleProps } from './module';
|
||||
import { BasicGaugeColor, Threshold } from 'app/types';
|
||||
|
||||
interface State {
|
||||
thresholds: Threshold[];
|
||||
baseColor: string;
|
||||
}
|
||||
|
||||
export default class Thresholds extends PureComponent<OptionModuleProps, State> {
|
||||
@@ -14,10 +15,12 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
||||
|
||||
this.state = {
|
||||
thresholds: props.options.thresholds,
|
||||
baseColor: props.options.baseColor,
|
||||
};
|
||||
}
|
||||
|
||||
onAddThreshold = index => {
|
||||
const { maxValue, minValue } = this.props.options;
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(threshold => {
|
||||
@@ -28,15 +31,28 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
||||
return threshold;
|
||||
});
|
||||
|
||||
// Setting value to a value between the new threshold.
|
||||
const value = newThresholds[index].value - (newThresholds[index].value - newThresholds[index - 1].value) / 2;
|
||||
// Setting value to a value between the previous thresholds
|
||||
let value;
|
||||
|
||||
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, BasicGaugeColor.Red, 50).toRgbString();
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: this.sortThresholds([
|
||||
...newThresholds,
|
||||
{ index: index, label: '', value: value, canRemove: true, color: BasicGaugeColor.Orange },
|
||||
]),
|
||||
thresholds: this.sortThresholds([...newThresholds, { index: index, value: value, color: color }]),
|
||||
},
|
||||
() => this.updateGauge()
|
||||
);
|
||||
@@ -86,6 +102,7 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
||||
);
|
||||
};
|
||||
|
||||
onChangeBaseColor = color => this.props.onChange({ ...this.props.options, baseColor: color });
|
||||
onBlur = () => {
|
||||
this.setState(prevState => ({
|
||||
thresholds: this.sortThresholds(prevState.thresholds),
|
||||
@@ -100,78 +117,16 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
||||
|
||||
sortThresholds = thresholds => {
|
||||
return thresholds.sort((t1, t2) => {
|
||||
return t1.value - t2.value;
|
||||
return t2.value - t1.value;
|
||||
});
|
||||
};
|
||||
|
||||
getIndicatorColor = index => {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
if (index === 0) {
|
||||
return thresholds[0].color;
|
||||
}
|
||||
|
||||
return index < thresholds.length ? thresholds[index].color : BasicGaugeColor.Red;
|
||||
};
|
||||
|
||||
renderNoThresholds() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const min = thresholds[0];
|
||||
const max = thresholds[1];
|
||||
|
||||
return [
|
||||
<div className="threshold-row threshold-row-min" key="min">
|
||||
<div className="threshold-row-inner">
|
||||
<div className="threshold-row-color">
|
||||
<div className="threshold-row-color-inner">
|
||||
<ColorPicker color={min.color} onChange={color => this.onChangeThresholdColor(min, color)} />
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
className="threshold-row-input"
|
||||
onBlur={this.onBlur}
|
||||
onChange={event => this.onChangeThresholdValue(event, min)}
|
||||
value={min.value}
|
||||
/>
|
||||
<div className="threshold-row-label">{min.label}</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div className="threshold-row" key="add">
|
||||
<div className="threshold-row-inner">
|
||||
<div onClick={() => this.onAddThreshold(1)} className="threshold-row-add">
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div className="threshold-row-add-label">Add new threshold by clicking the line.</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div className="threshold-row threshold-row-max" key="max">
|
||||
<div className="threshold-row-inner">
|
||||
<div className="threshold-row-color" />
|
||||
<input
|
||||
className="threshold-row-input"
|
||||
onBlur={this.onBlur}
|
||||
onChange={event => this.onChangeThresholdValue(event, max)}
|
||||
value={max.value}
|
||||
/>
|
||||
<div className="threshold-row-label">{max.label}</div>
|
||||
</div>
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
||||
renderThresholds() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return thresholds.map((threshold, index) => {
|
||||
const rowStyle = classNames({
|
||||
'threshold-row': true,
|
||||
'threshold-row-min': index === 0,
|
||||
'threshold-row-max': index === thresholds.length - 1,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={rowStyle} key={`${threshold.index}-${index}`}>
|
||||
<div className="threshold-row" key={`${threshold.index}-${index}`}>
|
||||
<div className="threshold-row-inner">
|
||||
<div className="threshold-row-color">
|
||||
{threshold.color && (
|
||||
@@ -190,103 +145,80 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
|
||||
value={threshold.value}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
{threshold.canRemove ? (
|
||||
<div onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
|
||||
<i className="fa fa-times" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="threshold-row-label">{threshold.label}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
insertAtIndex(index) {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
// If thresholds.length is greater or equal to 3
|
||||
// it means a user has added one threshold
|
||||
if (thresholds.length < 3 || index < 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
renderIndicatorSection(index) {
|
||||
const { thresholds } = this.state;
|
||||
const indicators = thresholds.length - 1;
|
||||
|
||||
if (index === 0 || index === thresholds.length) {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="indicator-section"
|
||||
style={{
|
||||
height: `calc(100%/${indicators})`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => this.onAddThreshold(this.insertAtIndex(index - 1))}
|
||||
style={{
|
||||
height: '100%',
|
||||
background: this.getIndicatorColor(index),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="indicator-section"
|
||||
style={{
|
||||
height: `calc(100%/${indicators})`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => this.onAddThreshold(this.insertAtIndex(index))}
|
||||
style={{
|
||||
height: '50%',
|
||||
background: this.getIndicatorColor(index),
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
onClick={() => this.onAddThreshold(this.insertAtIndex(index + 1))}
|
||||
style={{
|
||||
height: `50%`,
|
||||
background: this.getIndicatorColor(index),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderIndicator() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return thresholds.map((t, i) => {
|
||||
if (i <= thresholds.length - 1) {
|
||||
return this.renderIndicatorSection(i);
|
||||
}
|
||||
|
||||
return null;
|
||||
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>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { thresholds } = this.state;
|
||||
renderBaseIndicator() {
|
||||
return (
|
||||
<div className="indicator-section" style={{ height: '100%' }}>
|
||||
<div
|
||||
onClick={() => this.onAddThreshold(0)}
|
||||
style={{ height: '100%', backgroundColor: this.props.options.baseColor }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderBase() {
|
||||
const { baseColor } = this.props.options;
|
||||
|
||||
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() {
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Thresholds</h5>
|
||||
<h5 className="section-heading">Thresholds</h5>
|
||||
<div className="thresholds">
|
||||
<div className="color-indicators">{this.renderIndicator()}</div>
|
||||
<div className="color-indicators">
|
||||
{this.renderIndicator()}
|
||||
{this.renderBaseIndicator()}
|
||||
</div>
|
||||
<div className="threshold-rows">
|
||||
{thresholds.length > 2 ? this.renderThresholds() : this.renderNoThresholds()}
|
||||
{this.renderThresholds()}
|
||||
{this.renderBase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ export default class ValueMappings extends PureComponent<OptionModuleProps, Stat
|
||||
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Value mappings</h5>
|
||||
<h5 className="section-heading">Value mappings</h5>
|
||||
<div>
|
||||
{mappings.length > 0 &&
|
||||
mappings.map((mapping, index) => (
|
||||
|
||||
@@ -40,8 +40,8 @@ export default class ValueOptions extends PureComponent<OptionModuleProps> {
|
||||
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Value</h5>
|
||||
<div className="gf-form-inline">
|
||||
<h5 className="section-heading">Value</h5>
|
||||
<div className="gf-form">
|
||||
<Label width={labelWidth}>Stat</Label>
|
||||
<Select
|
||||
width={12}
|
||||
@@ -50,11 +50,11 @@ export default class ValueOptions extends PureComponent<OptionModuleProps> {
|
||||
value={statOptions.find(option => option.value === stat)}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<Label width={labelWidth}>Unit</Label>
|
||||
<UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<Label width={labelWidth}>Decimals</Label>
|
||||
<input
|
||||
className="gf-form-input width-12"
|
||||
@@ -64,11 +64,11 @@ export default class ValueOptions extends PureComponent<OptionModuleProps> {
|
||||
onChange={this.onDecimalChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<Label width={labelWidth}>Prefix</Label>
|
||||
<input className="gf-form-input width-12" type="text" value={prefix || ''} onChange={this.onPrefixChange} />
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<Label width={labelWidth}>Suffix</Label>
|
||||
<input className="gf-form-input width-12" type="text" value={suffix || ''} onChange={this.onSuffixChange} />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`Render should render component 1`] = `
|
||||
className="section gf-form-group"
|
||||
>
|
||||
<h5
|
||||
className="page-heading"
|
||||
className="section-heading"
|
||||
>
|
||||
Value mappings
|
||||
</h5>
|
||||
|
||||
@@ -16,15 +16,18 @@ import {
|
||||
} from 'app/types';
|
||||
|
||||
export interface OptionsProps {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
unit: string;
|
||||
thresholds: Threshold[];
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface OptionModuleProps {
|
||||
@@ -34,6 +37,7 @@ export interface OptionModuleProps {
|
||||
|
||||
export const defaultProps = {
|
||||
options: {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
prefix: '',
|
||||
@@ -44,10 +48,7 @@ export const defaultProps = {
|
||||
stat: '',
|
||||
unit: '',
|
||||
mappings: [],
|
||||
thresholds: [
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
||||
{ index: 1, label: 'Max', value: 100, canRemove: false },
|
||||
],
|
||||
thresholds: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -31,10 +31,8 @@ export interface PanelMenuItem {
|
||||
|
||||
export interface Threshold {
|
||||
index: number;
|
||||
label: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
canRemove: boolean;
|
||||
}
|
||||
|
||||
export enum MappingType {
|
||||
@@ -43,9 +41,8 @@ export enum MappingType {
|
||||
}
|
||||
|
||||
export enum BasicGaugeColor {
|
||||
Green = 'rgba(50, 172, 45, 0.97)',
|
||||
Orange = 'rgba(237, 129, 40, 0.89)',
|
||||
Red = 'rgb(212, 74, 58)',
|
||||
Green = '#299c46',
|
||||
Red = '#d44a3a',
|
||||
}
|
||||
|
||||
interface BaseMap {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } from 'app/types';
|
||||
import { BasicGaugeColor, MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } from 'app/types';
|
||||
import config from '../core/config';
|
||||
import kbn from '../core/utils/kbn';
|
||||
|
||||
interface Props {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
height: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
@@ -25,6 +26,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
canvasElement: any;
|
||||
|
||||
static defaultProps = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
maxValue: 100,
|
||||
mappings: [],
|
||||
minValue: 0,
|
||||
@@ -32,11 +34,9 @@ export class Gauge extends PureComponent<Props> {
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
thresholds: [
|
||||
{ label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
|
||||
{ label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
|
||||
],
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -92,12 +92,44 @@ export class Gauge extends PureComponent<Props> {
|
||||
return `${prefix} ${formattedValue} ${suffix}`;
|
||||
}
|
||||
|
||||
getFontColor(value) {
|
||||
const { baseColor, maxValue, thresholds } = this.props;
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => value <= threshold.value);
|
||||
|
||||
if (atThreshold.length > 0) {
|
||||
return atThreshold[0].color;
|
||||
} else if (value <= maxValue) {
|
||||
return BasicGaugeColor.Red;
|
||||
}
|
||||
|
||||
return baseColor;
|
||||
}
|
||||
|
||||
draw() {
|
||||
const { timeSeries, showThresholdLabels, showThresholdMarkers, thresholds, width, height, stat } = this.props;
|
||||
const {
|
||||
baseColor,
|
||||
maxValue,
|
||||
minValue,
|
||||
timeSeries,
|
||||
showThresholdLabels,
|
||||
showThresholdMarkers,
|
||||
thresholds,
|
||||
width,
|
||||
height,
|
||||
stat,
|
||||
} = this.props;
|
||||
|
||||
let value: string | number = '';
|
||||
|
||||
if (timeSeries[0]) {
|
||||
value = timeSeries[0].stats[stat];
|
||||
} else {
|
||||
value = 'N/A';
|
||||
}
|
||||
|
||||
const dimension = Math.min(width, height * 1.3);
|
||||
const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
||||
const fontColor = config.bootData.user.lightTheme ? 'rgb(38,38,38)' : 'rgb(230,230,230)';
|
||||
const fontScale = parseInt('80', 10) / 100;
|
||||
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
@@ -105,20 +137,26 @@ export class Gauge extends PureComponent<Props> {
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const formattedThresholds = thresholds.map((threshold, index) => {
|
||||
const formattedThresholds = [
|
||||
{ value: minValue, color: BasicGaugeColor.Green },
|
||||
...thresholds.map((threshold, index) => {
|
||||
return {
|
||||
value: threshold.value,
|
||||
// Hacky way to get correct color for threshold.
|
||||
color: index === 0 ? threshold.color : thresholds[index - 1].color,
|
||||
color: index === 0 ? threshold.color : thresholds[index].color,
|
||||
};
|
||||
});
|
||||
}),
|
||||
{
|
||||
value: maxValue,
|
||||
color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor,
|
||||
},
|
||||
];
|
||||
|
||||
const options = {
|
||||
series: {
|
||||
gauges: {
|
||||
gauge: {
|
||||
min: thresholds[0].value,
|
||||
max: thresholds[thresholds.length - 1].value,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
background: { color: backgroundColor },
|
||||
border: { color: null },
|
||||
shadow: { show: false },
|
||||
@@ -139,13 +177,9 @@ export class Gauge extends PureComponent<Props> {
|
||||
width: thresholdMarkersWidth,
|
||||
},
|
||||
value: {
|
||||
color: fontColor,
|
||||
color: this.getFontColor(value),
|
||||
formatter: () => {
|
||||
if (timeSeries[0]) {
|
||||
return this.formatValue(timeSeries[0].stats[stat]);
|
||||
}
|
||||
|
||||
return '';
|
||||
return this.formatValue(value);
|
||||
},
|
||||
font: {
|
||||
size: fontSize,
|
||||
@@ -157,11 +191,6 @@ export class Gauge extends PureComponent<Props> {
|
||||
},
|
||||
};
|
||||
|
||||
let value: string | number = 'N/A';
|
||||
if (timeSeries.length) {
|
||||
value = timeSeries[0].stats[stat];
|
||||
}
|
||||
|
||||
const plotSeries = {
|
||||
data: [[0, value]],
|
||||
};
|
||||
|
||||
@@ -123,6 +123,15 @@ $input-border: 1px solid $input-border-color;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&--btn {
|
||||
border-right: $input-btn-border-width solid $input-label-border-color;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&:hover {
|
||||
background: $list-item-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $text-color-weak;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.thresholds {
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.threshold-rows {
|
||||
@@ -10,7 +9,7 @@
|
||||
.threshold-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
margin-top: 3px;
|
||||
padding: 5px;
|
||||
|
||||
&::before {
|
||||
@@ -25,8 +24,11 @@
|
||||
border-radius: $border-radius;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 300px;
|
||||
height: 37px;
|
||||
|
||||
&--base {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-row-color {
|
||||
@@ -48,13 +50,12 @@
|
||||
|
||||
.threshold-row-input {
|
||||
padding: 8px 10px;
|
||||
width: 230px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.threshold-row-label {
|
||||
background-color: $input-label-bg;
|
||||
padding: 5px;
|
||||
width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -65,12 +66,7 @@
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.threshold-row-min {
|
||||
margin-top: -22px;
|
||||
}
|
||||
|
||||
.threshold-row-max {
|
||||
margin-bottom: -22px;
|
||||
.threshold-row-base {
|
||||
}
|
||||
|
||||
.threshold-row-remove {
|
||||
@@ -98,11 +94,13 @@
|
||||
|
||||
.indicator-section {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-indicators {
|
||||
width: 15px;
|
||||
border-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,6 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mapping-row-type {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mapping-row-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.add-mapping-row {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
Reference in New Issue
Block a user