mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14461 from grafana/gauge-value-mappings
Gauge value mappings
This commit is contained in:
commit
8994f3c283
@ -11,7 +11,7 @@ interface Props {
|
|||||||
onSelected: (item: any) => {} | void;
|
onSelected: (item: any) => {} | void;
|
||||||
options: any[];
|
options: any[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
width: number;
|
width?: number;
|
||||||
value: any;
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,8 +28,8 @@ const SimplePicker: SFC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
classNamePrefix={`gf-form-select-box`}
|
classNamePrefix="gf-form-select-box"
|
||||||
className={`width-${width} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
className={`${width ? 'width-' + width : ''} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
|
||||||
components={{
|
components={{
|
||||||
Option: DescriptionOption,
|
Option: DescriptionOption,
|
||||||
}}
|
}}
|
||||||
|
@ -63,7 +63,7 @@ export default class UnitPicker extends PureComponent<Props> {
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
classNamePrefix="gf-form-select-box"
|
classNamePrefix="gf-form-select-box"
|
||||||
className={`width-${width} gf-form-input--form-dropdown`}
|
className={`width-${width} gf-form-input gf-form-input--form-dropdown`}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
menuShouldScrollIntoView={false}
|
menuShouldScrollIntoView={false}
|
||||||
|
155
public/app/plugins/panel/gauge/MappingRow.tsx
Normal file
155
public/app/plugins/panel/gauge/MappingRow.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { Label } from 'app/core/components/Label/Label';
|
||||||
|
import SimplePicker from 'app/core/components/Picker/SimplePicker';
|
||||||
|
import { MappingType, RangeMap, ValueMap } from 'app/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mapping: ValueMap | RangeMap;
|
||||||
|
updateMapping: (mapping) => void;
|
||||||
|
removeMapping: () => 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) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
...props.mapping,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMappingValueChange = event => {
|
||||||
|
this.setState({ value: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMappingFromChange = event => {
|
||||||
|
this.setState({ from: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMappingToChange = event => {
|
||||||
|
this.setState({ to: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMappingTextChange = event => {
|
||||||
|
this.setState({ text: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onMappingTypeChange = mappingType => {
|
||||||
|
this.setState({ type: mappingType });
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMapping = () => {
|
||||||
|
this.props.updateMapping({ ...this.state });
|
||||||
|
};
|
||||||
|
|
||||||
|
renderRow() {
|
||||||
|
const { from, text, to, type, value } = this.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"
|
||||||
|
value={from}
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingFromChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-inline mapping-row-input">
|
||||||
|
<Label width={4}>To</Label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
value={to}
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingToChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-inline mapping-row-input">
|
||||||
|
<Label width={4}>Text</Label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
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"
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingValueChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form-inline mapping-row-input">
|
||||||
|
<Label width={4}>Text</Label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="gf-form-input"
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
value={text}
|
||||||
|
onChange={this.onMappingTextChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { type } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mapping-row">
|
||||||
|
<div className="gf-form-inline mapping-row-type">
|
||||||
|
<Label width={5}>Type</Label>
|
||||||
|
<SimplePicker
|
||||||
|
placeholder="Choose type"
|
||||||
|
options={mappingOptions}
|
||||||
|
value={mappingOptions.find(o => o.value === type)}
|
||||||
|
getOptionLabel={i => i.label}
|
||||||
|
getOptionValue={i => i.value}
|
||||||
|
onSelected={type => this.onMappingTypeChange(type.value)}
|
||||||
|
width={7}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>{this.renderRow()}</div>
|
||||||
|
<div onClick={this.props.removeMapping} className="threshold-row-remove">
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import Thresholds from './Thresholds';
|
import Thresholds from './Thresholds';
|
||||||
import { OptionsProps } from './module';
|
import { defaultProps, OptionsProps } from './module';
|
||||||
import { PanelOptionsProps } from '../../../types';
|
import { PanelOptionsProps } from '../../../types';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: PanelOptionsProps<OptionsProps> = {
|
const props: PanelOptionsProps<OptionsProps> = {
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
options: {} as OptionsProps,
|
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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
@ -15,12 +21,6 @@ const setup = (propOverrides?: object) => {
|
|||||||
return shallow(<Thresholds {...props} />).instance() as Thresholds;
|
return shallow(<Thresholds {...props} />).instance() as Thresholds;
|
||||||
};
|
};
|
||||||
|
|
||||||
const 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('Add threshold', () => {
|
describe('Add threshold', () => {
|
||||||
it('should add threshold between min and max', () => {
|
it('should add threshold between min and max', () => {
|
||||||
const instance = setup();
|
const instance = setup();
|
||||||
@ -36,7 +36,14 @@ describe('Add threshold', () => {
|
|||||||
|
|
||||||
it('should add threshold between min and added threshold', () => {
|
it('should add threshold between min and added threshold', () => {
|
||||||
const instance = setup({
|
const instance = setup({
|
||||||
options: { thresholds: thresholds },
|
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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.onAddThreshold(1);
|
instance.onAddThreshold(1);
|
||||||
|
@ -2,27 +2,18 @@ import React, { PureComponent } from 'react';
|
|||||||
import classNames from 'classnames/bind';
|
import classNames from 'classnames/bind';
|
||||||
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
||||||
import { OptionModuleProps } from './module';
|
import { OptionModuleProps } from './module';
|
||||||
import { Threshold } from 'app/types';
|
import { BasicGaugeColor, Threshold } from 'app/types';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BasicGaugeColor {
|
|
||||||
Green = 'rgba(50, 172, 45, 0.97)',
|
|
||||||
Orange = 'rgba(237, 129, 40, 0.89)',
|
|
||||||
Red = 'rgb(212, 74, 58)',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Thresholds extends PureComponent<OptionModuleProps, State> {
|
export default class Thresholds extends PureComponent<OptionModuleProps, State> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
thresholds: this.props.options.thresholds || [
|
thresholds: props.options.thresholds,
|
||||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
|
||||||
{ index: 1, label: 'Max', value: 100, canRemove: false },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
73
public/app/plugins/panel/gauge/ValueMappings.test.tsx
Normal file
73
public/app/plugins/panel/gauge/ValueMappings.test.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import ValueMappings from './ValueMappings';
|
||||||
|
import { defaultProps, OptionModuleProps } from './module';
|
||||||
|
import { MappingType } from 'app/types';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: OptionModuleProps = {
|
||||||
|
onChange: jest.fn(),
|
||||||
|
options: {
|
||||||
|
...defaultProps.options,
|
||||||
|
mappings: [
|
||||||
|
{ 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(<ValueMappings {...props} />);
|
||||||
|
|
||||||
|
const instance = wrapper.instance() as ValueMappings;
|
||||||
|
|
||||||
|
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.mappings).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.mappings).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({ options: { ...defaultProps.options } });
|
||||||
|
|
||||||
|
expect(instance.state.nextIdToAdd).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
100
public/app/plugins/panel/gauge/ValueMappings.tsx
Normal file
100
public/app/plugins/panel/gauge/ValueMappings.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import MappingRow from './MappingRow';
|
||||||
|
import { OptionModuleProps } from './module';
|
||||||
|
import { MappingType, RangeMap, ValueMap } from 'app/types';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
mappings: Array<ValueMap | RangeMap>;
|
||||||
|
nextIdToAdd: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ValueMappings extends PureComponent<OptionModuleProps, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const mappings = props.options.mappings;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
mappings: mappings || [],
|
||||||
|
nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromMappings(mappings) : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxIdFromMappings(mappings) {
|
||||||
|
return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMapping = () =>
|
||||||
|
this.setState(prevState => ({
|
||||||
|
mappings: [
|
||||||
|
...prevState.mappings,
|
||||||
|
{
|
||||||
|
id: prevState.nextIdToAdd,
|
||||||
|
operator: '',
|
||||||
|
value: '',
|
||||||
|
text: '',
|
||||||
|
type: MappingType.ValueToText,
|
||||||
|
from: '',
|
||||||
|
to: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nextIdToAdd: prevState.nextIdToAdd + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
onRemoveMapping = id => {
|
||||||
|
this.setState(
|
||||||
|
prevState => ({
|
||||||
|
mappings: prevState.mappings.filter(m => {
|
||||||
|
return m.id !== id;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateGauge = mapping => {
|
||||||
|
this.setState(
|
||||||
|
prevState => ({
|
||||||
|
mappings: prevState.mappings.map(m => {
|
||||||
|
if (m.id === mapping.id) {
|
||||||
|
return { ...mapping };
|
||||||
|
}
|
||||||
|
|
||||||
|
return m;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { mappings } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="section gf-form-group">
|
||||||
|
<h5 className="page-heading">Value mappings</h5>
|
||||||
|
<div>
|
||||||
|
{mappings.length > 0 &&
|
||||||
|
mappings.map((mapping, index) => (
|
||||||
|
<MappingRow
|
||||||
|
key={`${mapping.text}-${index}`}
|
||||||
|
mapping={mapping}
|
||||||
|
updateMapping={this.updateGauge}
|
||||||
|
removeMapping={() => this.onRemoveMapping(mapping.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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="section gf-form-group"
|
||||||
|
>
|
||||||
|
<h5
|
||||||
|
className="page-heading"
|
||||||
|
>
|
||||||
|
Value mappings
|
||||||
|
</h5>
|
||||||
|
<div>
|
||||||
|
<MappingRow
|
||||||
|
key="Ok-0"
|
||||||
|
mapping={
|
||||||
|
Object {
|
||||||
|
"id": 1,
|
||||||
|
"operator": "",
|
||||||
|
"text": "Ok",
|
||||||
|
"type": 1,
|
||||||
|
"value": "20",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeMapping={[Function]}
|
||||||
|
updateMapping={[Function]}
|
||||||
|
/>
|
||||||
|
<MappingRow
|
||||||
|
key="Meh-1"
|
||||||
|
mapping={
|
||||||
|
Object {
|
||||||
|
"from": "21",
|
||||||
|
"id": 2,
|
||||||
|
"operator": "",
|
||||||
|
"text": "Meh",
|
||||||
|
"to": "30",
|
||||||
|
"type": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeMapping={[Function]}
|
||||||
|
updateMapping={[Function]}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -1,10 +1,19 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import Gauge from 'app/viz/Gauge';
|
import Gauge from 'app/viz/Gauge';
|
||||||
import { NullValueMode, PanelOptionsProps, PanelProps, Threshold } from 'app/types';
|
|
||||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||||
import ValueOptions from './ValueOptions';
|
import ValueOptions from './ValueOptions';
|
||||||
import GaugeOptions from './GaugeOptions';
|
import GaugeOptions from './GaugeOptions';
|
||||||
import Thresholds from './Thresholds';
|
import Thresholds from './Thresholds';
|
||||||
|
import ValueMappings from './ValueMappings';
|
||||||
|
import {
|
||||||
|
BasicGaugeColor,
|
||||||
|
NullValueMode,
|
||||||
|
PanelOptionsProps,
|
||||||
|
PanelProps,
|
||||||
|
RangeMap,
|
||||||
|
Threshold,
|
||||||
|
ValueMap,
|
||||||
|
} from 'app/types';
|
||||||
|
|
||||||
export interface OptionsProps {
|
export interface OptionsProps {
|
||||||
decimals: number;
|
decimals: number;
|
||||||
@ -15,6 +24,7 @@ export interface OptionsProps {
|
|||||||
suffix: string;
|
suffix: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
|
mappings: Array<RangeMap | ValueMap>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OptionModuleProps {
|
export interface OptionModuleProps {
|
||||||
@ -30,6 +40,14 @@ export const defaultProps = {
|
|||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
showThresholdLabels: false,
|
showThresholdLabels: false,
|
||||||
suffix: '',
|
suffix: '',
|
||||||
|
decimals: 0,
|
||||||
|
stat: '',
|
||||||
|
unit: '',
|
||||||
|
mappings: [],
|
||||||
|
thresholds: [
|
||||||
|
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
||||||
|
{ index: 1, label: 'Max', value: 100, canRemove: false },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,11 +70,17 @@ class Options extends PureComponent<PanelOptionsProps<OptionsProps>> {
|
|||||||
static defaultProps = defaultProps;
|
static defaultProps = defaultProps;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { onChange, options } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ValueOptions onChange={this.props.onChange} options={this.props.options} />
|
<div className="form-section">
|
||||||
<GaugeOptions onChange={this.props.onChange} options={this.props.options} />
|
<ValueOptions onChange={onChange} options={options} />
|
||||||
<Thresholds onChange={this.props.onChange} options={this.props.options} />
|
<GaugeOptions onChange={onChange} options={options} />
|
||||||
|
<Thresholds onChange={onChange} options={options} />
|
||||||
|
</div>
|
||||||
|
<div className="form-section">
|
||||||
|
<ValueMappings onChange={onChange} options={options} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
DataQueryOptions,
|
DataQueryOptions,
|
||||||
IntervalValues,
|
IntervalValues,
|
||||||
} from './series';
|
} from './series';
|
||||||
import { PanelProps, PanelOptionsProps, Threshold } from './panel';
|
import { BasicGaugeColor, MappingType, PanelProps, PanelOptionsProps, RangeMap, Threshold, ValueMap } from './panel';
|
||||||
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
|
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
|
||||||
import { Organization, OrganizationState } from './organization';
|
import { Organization, OrganizationState } from './organization';
|
||||||
import {
|
import {
|
||||||
@ -93,7 +93,11 @@ export {
|
|||||||
Threshold,
|
Threshold,
|
||||||
ValidationEvents,
|
ValidationEvents,
|
||||||
ValidationRule,
|
ValidationRule,
|
||||||
|
ValueMap,
|
||||||
|
RangeMap,
|
||||||
IntervalValues,
|
IntervalValues,
|
||||||
|
MappingType,
|
||||||
|
BasicGaugeColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
|
@ -36,3 +36,30 @@ export interface Threshold {
|
|||||||
color?: string;
|
color?: string;
|
||||||
canRemove: boolean;
|
canRemove: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum MappingType {
|
||||||
|
ValueToText = 1,
|
||||||
|
RangeToText = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BasicGaugeColor {
|
||||||
|
Green = 'rgba(50, 172, 45, 0.97)',
|
||||||
|
Orange = 'rgba(237, 129, 40, 0.89)',
|
||||||
|
Red = 'rgb(212, 74, 58)',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseMap {
|
||||||
|
id: number;
|
||||||
|
operator: string;
|
||||||
|
text: string;
|
||||||
|
type: MappingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValueMap extends BaseMap {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RangeMap extends BaseMap {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
@ -1,38 +1,42 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { Threshold, TimeSeriesVMs } from 'app/types';
|
import { MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } from 'app/types';
|
||||||
import config from '../core/config';
|
import config from '../core/config';
|
||||||
import kbn from '../core/utils/kbn';
|
import kbn from '../core/utils/kbn';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
decimals: number;
|
decimals: number;
|
||||||
|
height: number;
|
||||||
|
mappings: Array<RangeMap | ValueMap>;
|
||||||
|
maxValue: number;
|
||||||
|
minValue: number;
|
||||||
|
prefix: string;
|
||||||
timeSeries: TimeSeriesVMs;
|
timeSeries: TimeSeriesVMs;
|
||||||
showThresholdMarkers: boolean;
|
|
||||||
thresholds: Threshold[];
|
thresholds: Threshold[];
|
||||||
|
showThresholdMarkers: boolean;
|
||||||
showThresholdLabels: boolean;
|
showThresholdLabels: boolean;
|
||||||
|
stat: string;
|
||||||
|
suffix: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
|
||||||
stat: string;
|
|
||||||
prefix: string;
|
|
||||||
suffix: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Gauge extends PureComponent<Props> {
|
export class Gauge extends PureComponent<Props> {
|
||||||
canvasElement: any;
|
canvasElement: any;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
minValue: 0,
|
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
|
mappings: [],
|
||||||
|
minValue: 0,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
showThresholdLabels: false,
|
showThresholdLabels: false,
|
||||||
suffix: '',
|
suffix: '',
|
||||||
unit: 'none',
|
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{ label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
|
{ label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
|
||||||
{ label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
|
{ label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
|
||||||
],
|
],
|
||||||
|
unit: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -43,16 +47,49 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatWithMappings(mappings, value) {
|
||||||
|
const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
|
||||||
|
const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
|
||||||
|
|
||||||
|
const valueMap = valueMaps.map(mapping => {
|
||||||
|
if (mapping.value && value === mapping.value) {
|
||||||
|
return mapping.text;
|
||||||
|
}
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
const rangeMap = rangeMaps.map(mapping => {
|
||||||
|
if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
|
||||||
|
return mapping.text;
|
||||||
|
}
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
rangeMap,
|
||||||
|
valueMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
formatValue(value) {
|
formatValue(value) {
|
||||||
const { decimals, prefix, suffix, unit } = this.props;
|
const { decimals, mappings, prefix, suffix, unit } = this.props;
|
||||||
|
|
||||||
const formatFunc = kbn.valueFormats[unit];
|
const formatFunc = kbn.valueFormats[unit];
|
||||||
|
const formattedValue = formatFunc(value, decimals);
|
||||||
|
|
||||||
|
if (mappings.length > 0) {
|
||||||
|
const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue);
|
||||||
|
|
||||||
|
if (valueMap) {
|
||||||
|
return valueMap;
|
||||||
|
} else if (rangeMap) {
|
||||||
|
return rangeMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${prefix} ${formatFunc(value, decimals)} ${suffix}`;
|
return `${prefix} ${formattedValue} ${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
|
@ -106,6 +106,7 @@
|
|||||||
@import 'components/unit-picker';
|
@import 'components/unit-picker';
|
||||||
@import 'components/thresholds';
|
@import 'components/thresholds';
|
||||||
@import 'components/toggle_button_group';
|
@import 'components/toggle_button_group';
|
||||||
|
@import 'components/value-mappings';
|
||||||
|
|
||||||
// PAGES
|
// PAGES
|
||||||
@import 'pages/login';
|
@import 'pages/login';
|
||||||
|
@ -90,7 +90,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
|
|
||||||
.gf-form-select-box__value-container {
|
.gf-form-select-box__value-container {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
padding: 8px 10px;
|
padding: 6px 10px;
|
||||||
> div {
|
> div {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 36px;
|
height: 37px;
|
||||||
|
width: 37px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
public/sass/components/_value-mappings.scss
Normal file
37
public/sass/components/_value-mappings.scss
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
.mapping-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-row-type {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-row-input {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user