mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14234 from grafana/gauge-value-options
Gauge value options
This commit is contained in:
commit
c2aa64595a
@ -6,6 +6,7 @@ interface Props {
|
||||
for?: string;
|
||||
children: ReactNode;
|
||||
width?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Label: SFC<Props> = props => {
|
||||
|
@ -14,6 +14,16 @@ export default class UnitGroup extends PureComponent<ExtendedGroupProps, State>
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.selectProps) {
|
||||
const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
|
||||
|
||||
if (value && this.props.options.some(option => option.value === value)) {
|
||||
this.setState({ expanded: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(nextProps) {
|
||||
if (nextProps.selectProps.inputValue !== '') {
|
||||
this.setState({ expanded: true });
|
||||
|
@ -8,11 +8,16 @@ import kbn from '../../../utils/kbn';
|
||||
interface Props {
|
||||
onSelected: (item: any) => {} | void;
|
||||
defaultValue?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export default class UnitPicker extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
width: 12,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { defaultValue, onSelected } = this.props;
|
||||
const { defaultValue, onSelected, width } = this.props;
|
||||
|
||||
const unitGroups = kbn.getUnitFormats();
|
||||
|
||||
@ -42,6 +47,13 @@ export default class UnitPicker extends PureComponent<Props> {
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
} as React.CSSProperties),
|
||||
valueContainer: () =>
|
||||
({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '90px',
|
||||
whiteSpace: 'nowrap',
|
||||
} as React.CSSProperties),
|
||||
};
|
||||
|
||||
const value = groupOptions.map(group => {
|
||||
@ -51,7 +63,7 @@ export default class UnitPicker extends PureComponent<Props> {
|
||||
return (
|
||||
<Select
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className="width-20 gf-form-input--form-dropdown"
|
||||
className={`width-${width} gf-form-input--form-dropdown`}
|
||||
defaultValue={value}
|
||||
isSearchable={true}
|
||||
menuShouldScrollIntoView={false}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import $ from 'jquery';
|
||||
import Drop from 'tether-drop';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
@ -11,29 +10,17 @@ export interface Props {
|
||||
}
|
||||
|
||||
export class ColorPicker extends React.Component<Props, any> {
|
||||
pickerElem: any;
|
||||
pickerElem: HTMLElement;
|
||||
colorPickerDrop: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.openColorPicker = this.openColorPicker.bind(this);
|
||||
this.closeColorPicker = this.closeColorPicker.bind(this);
|
||||
this.setPickerElem = this.setPickerElem.bind(this);
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
setPickerElem(elem) {
|
||||
this.pickerElem = $(elem);
|
||||
}
|
||||
|
||||
openColorPicker() {
|
||||
openColorPicker = () => {
|
||||
const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
|
||||
|
||||
const dropContentElem = document.createElement('div');
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
const drop = new Drop({
|
||||
target: this.pickerElem[0],
|
||||
target: this.pickerElem,
|
||||
content: dropContentElem,
|
||||
position: 'top center',
|
||||
classes: 'drop-popover',
|
||||
@ -48,23 +35,23 @@ export class ColorPicker extends React.Component<Props, any> {
|
||||
|
||||
this.colorPickerDrop = drop;
|
||||
this.colorPickerDrop.open();
|
||||
}
|
||||
};
|
||||
|
||||
closeColorPicker() {
|
||||
closeColorPicker = () => {
|
||||
setTimeout(() => {
|
||||
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
|
||||
this.colorPickerDrop.destroy();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
onColorSelect(color) {
|
||||
onColorSelect = color => {
|
||||
this.props.onChange(color);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={this.setPickerElem}>
|
||||
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={element => (this.pickerElem = element)}>
|
||||
<div className="sp-preview">
|
||||
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
|
||||
</div>
|
||||
|
@ -129,7 +129,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
const { dashboard, panel } = this.props;
|
||||
const { plugin } = this.state;
|
||||
|
||||
return <PanelChrome component={plugin.exports.Panel} panel={panel} dashboard={dashboard} />;
|
||||
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} />;
|
||||
}
|
||||
|
||||
renderAngularPanel() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Libraries
|
||||
import React, { ComponentClass, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
|
||||
// Services
|
||||
@ -16,12 +16,12 @@ import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
|
||||
// Types
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { TimeRange, PanelProps } from 'app/types';
|
||||
import { PanelPlugin, TimeRange } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
component: ComponentClass<PanelProps>;
|
||||
plugin: PanelPlugin;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@ -80,11 +80,11 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panel, dashboard } = this.props;
|
||||
const { panel, dashboard, plugin } = this.props;
|
||||
const { refreshCounter, timeRange, timeInfo, renderCounter } = this.state;
|
||||
|
||||
const { datasource, targets } = panel;
|
||||
const PanelComponent = this.props.component;
|
||||
const PanelComponent = plugin.exports.Panel;
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
@ -111,7 +111,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
loading={loading}
|
||||
timeSeries={timeSeries}
|
||||
timeRange={timeRange}
|
||||
options={panel.getOptions()}
|
||||
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
||||
width={width}
|
||||
height={height - PANEL_HEADER_HEIGHT}
|
||||
renderCounter={renderCounter}
|
||||
|
@ -25,12 +25,18 @@ export class VisualizationTab extends PureComponent<Props> {
|
||||
element: HTMLElement;
|
||||
angularOptions: AngularComponent;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
getPanelDefaultOptions = () => {
|
||||
const { panel, plugin } = this.props;
|
||||
|
||||
if (plugin.exports.PanelDefaults) {
|
||||
return panel.getOptions(plugin.exports.PanelDefaults.options);
|
||||
}
|
||||
|
||||
return panel.getOptions(plugin.exports.PanelDefaults);
|
||||
};
|
||||
|
||||
renderPanelOptions() {
|
||||
const { plugin, panel, angularPanel } = this.props;
|
||||
const { plugin, angularPanel } = this.props;
|
||||
const { PanelOptions } = plugin.exports;
|
||||
|
||||
if (angularPanel) {
|
||||
@ -38,7 +44,7 @@ export class VisualizationTab extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
if (PanelOptions) {
|
||||
return <PanelOptions options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||
return <PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||
} else {
|
||||
return <p>Visualization has no options</p>;
|
||||
}
|
||||
|
@ -108,8 +108,8 @@ export class PanelModel {
|
||||
_.defaultsDeep(this, _.cloneDeep(defaults));
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this[this.getOptionsKey()] || {};
|
||||
getOptions(panelDefaults) {
|
||||
return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
|
||||
}
|
||||
|
||||
updateOptions(options: object) {
|
||||
|
@ -1,14 +1,35 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { PanelOptionsProps } from 'app/types';
|
||||
import { Switch } from 'app/core/components/Switch/Switch';
|
||||
import { OptionModuleProps } from './module';
|
||||
|
||||
interface Props {}
|
||||
export default class GaugeOptions extends PureComponent<OptionModuleProps> {
|
||||
toggleThresholdLabels = () =>
|
||||
this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
|
||||
|
||||
toggleThresholdMarkers = () =>
|
||||
this.props.onChange({ ...this.props.options, showThresholdMarkers: !this.props.options.showThresholdMarkers });
|
||||
|
||||
export class GaugeOptions extends PureComponent<PanelOptionsProps<Props>> {
|
||||
render() {
|
||||
const { showThresholdLabels, showThresholdMarkers } = this.props.options;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Draw Modes</h5>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Gauge</h5>
|
||||
<div className="gf-form-inline">
|
||||
<Switch
|
||||
label="Threshold labels"
|
||||
labelClass="width-10"
|
||||
checked={showThresholdLabels}
|
||||
onChange={this.toggleThresholdLabels}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<Switch
|
||||
label="Threshold markers"
|
||||
labelClass="width-10"
|
||||
checked={showThresholdMarkers}
|
||||
onChange={this.toggleThresholdMarkers}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
135
public/app/plugins/panel/gauge/Threshold.test.tsx
Normal file
135
public/app/plugins/panel/gauge/Threshold.test.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import Thresholds from './Thresholds';
|
||||
import { OptionsProps } from './module';
|
||||
import { PanelOptionsProps } from '../../../types';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: PanelOptionsProps<OptionsProps> = {
|
||||
onChange: jest.fn(),
|
||||
options: {} as OptionsProps,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
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', () => {
|
||||
it('should add threshold between min and max', () => {
|
||||
const instance = setup();
|
||||
|
||||
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: 50, canRemove: true, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 2, label: 'Max', value: 100, canRemove: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add threshold between min and added threshold', () => {
|
||||
const instance = setup({
|
||||
options: { thresholds: thresholds },
|
||||
});
|
||||
|
||||
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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
instance.state = {
|
||||
thresholds: mockThresholds,
|
||||
};
|
||||
|
||||
const mockEvent = { target: { value: 78 } };
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, mockThresholds[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: 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 },
|
||||
]);
|
||||
});
|
||||
});
|
304
public/app/plugins/panel/gauge/Thresholds.tsx
Normal file
304
public/app/plugins/panel/gauge/Thresholds.tsx
Normal file
@ -0,0 +1,304 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
|
||||
import { OptionModuleProps } from './module';
|
||||
import { Threshold } from 'app/types';
|
||||
|
||||
interface State {
|
||||
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> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
thresholds: this.props.options.thresholds || [
|
||||
{ index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
|
||||
{ index: 1, label: 'Max', value: 100, canRemove: false },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
onAddThreshold = index => {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(threshold => {
|
||||
if (threshold.index >= index) {
|
||||
threshold = { ...threshold, index: threshold.index + 1 };
|
||||
}
|
||||
|
||||
return threshold;
|
||||
});
|
||||
|
||||
// Setting value to a value between the new threshold.
|
||||
const value = newThresholds[index].value - (newThresholds[index].value - newThresholds[index - 1].value) / 2;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: this.sortThresholds([
|
||||
...newThresholds,
|
||||
{ index: index, label: '', value: value, canRemove: true, color: BasicGaugeColor.Orange },
|
||||
]),
|
||||
},
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onRemoveThreshold = threshold => {
|
||||
this.setState(
|
||||
prevState => ({
|
||||
thresholds: prevState.thresholds.filter(t => t !== threshold),
|
||||
}),
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onChangeThresholdValue = (event, threshold) => {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
if (t === threshold) {
|
||||
t = { ...t, value: event.target.value };
|
||||
}
|
||||
|
||||
return t;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
thresholds: newThresholds,
|
||||
});
|
||||
};
|
||||
|
||||
onChangeThresholdColor = (threshold, color) => {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
if (t === threshold) {
|
||||
t = { ...t, color: color };
|
||||
}
|
||||
|
||||
return t;
|
||||
});
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: newThresholds,
|
||||
},
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this.setState(prevState => ({
|
||||
thresholds: this.sortThresholds(prevState.thresholds),
|
||||
}));
|
||||
|
||||
this.updateGauge();
|
||||
};
|
||||
|
||||
updateGauge = () => {
|
||||
this.props.onChange({ ...this.props.options, thresholds: this.state.thresholds });
|
||||
};
|
||||
|
||||
sortThresholds = thresholds => {
|
||||
return thresholds.sort((t1, t2) => {
|
||||
return t1.value - t2.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-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}
|
||||
/>
|
||||
{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;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Thresholds</h5>
|
||||
<div className="thresholds">
|
||||
<div className="color-indicators">{this.renderIndicator()}</div>
|
||||
<div className="threshold-rows">
|
||||
{thresholds.length > 2 ? this.renderThresholds() : this.renderNoThresholds()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
80
public/app/plugins/panel/gauge/ValueOptions.tsx
Normal file
80
public/app/plugins/panel/gauge/ValueOptions.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Label } from 'app/core/components/Label/Label';
|
||||
import SimplePicker from 'app/core/components/Picker/SimplePicker';
|
||||
import UnitPicker from 'app/core/components/Picker/Unit/UnitPicker';
|
||||
import { OptionModuleProps } from './module';
|
||||
|
||||
const statOptions = [
|
||||
{ value: 'min', text: 'Min' },
|
||||
{ value: 'max', text: 'Max' },
|
||||
{ value: 'avg', text: 'Average' },
|
||||
{ value: 'current', text: 'Current' },
|
||||
{ value: 'total', text: 'Total' },
|
||||
{ value: 'name', text: 'Name' },
|
||||
{ value: 'first', text: 'First' },
|
||||
{ value: 'delta', text: 'Delta' },
|
||||
{ value: 'diff', text: 'Difference' },
|
||||
{ value: 'range', text: 'Range' },
|
||||
{ value: 'last_time', text: 'Time of last point' },
|
||||
];
|
||||
|
||||
const labelWidth = 6;
|
||||
|
||||
export default class ValueOptions extends PureComponent<OptionModuleProps> {
|
||||
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
|
||||
|
||||
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
|
||||
|
||||
onDecimalChange = event => {
|
||||
if (!isNaN(event.target.value)) {
|
||||
this.props.onChange({ ...this.props.options, decimals: event.target.value });
|
||||
}
|
||||
};
|
||||
|
||||
onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value });
|
||||
|
||||
onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value });
|
||||
|
||||
render() {
|
||||
const { stat, unit, decimals, prefix, suffix } = this.props.options;
|
||||
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Value</h5>
|
||||
<div className="gf-form-inline">
|
||||
<Label width={labelWidth}>Stat</Label>
|
||||
<SimplePicker
|
||||
width={12}
|
||||
options={statOptions}
|
||||
getOptionLabel={i => i.text}
|
||||
getOptionValue={i => i.value}
|
||||
onSelected={this.onStatChange}
|
||||
value={statOptions.find(option => option.value === stat)}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<Label width={labelWidth}>Unit</Label>
|
||||
<UnitPicker defaultValue={unit} onSelected={value => this.onUnitChange(value)} />
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<Label width={labelWidth}>Decimals</Label>
|
||||
<input
|
||||
className="gf-form-input width-12"
|
||||
type="number"
|
||||
placeholder="auto"
|
||||
value={decimals || ''}
|
||||
onChange={this.onDecimalChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<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">
|
||||
<Label width={labelWidth}>Suffix</Label>
|
||||
<input className="gf-form-input width-12" type="text" value={suffix || ''} onChange={this.onSuffixChange} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,58 +1,65 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Gauge from 'app/viz/Gauge';
|
||||
import { Label } from 'app/core/components/Label/Label';
|
||||
import UnitPicker from 'app/core/components/Picker/Unit/UnitPicker';
|
||||
import { NullValueMode, PanelOptionsProps, PanelProps } from 'app/types';
|
||||
import { NullValueMode, PanelOptionsProps, PanelProps, Threshold } from 'app/types';
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
import ValueOptions from './ValueOptions';
|
||||
import GaugeOptions from './GaugeOptions';
|
||||
import Thresholds from './Thresholds';
|
||||
|
||||
export interface Options {
|
||||
unit: { label: string; value: string };
|
||||
export interface OptionsProps {
|
||||
decimals: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
unit: string;
|
||||
thresholds: Threshold[];
|
||||
}
|
||||
|
||||
interface Props extends PanelProps<Options> {}
|
||||
export interface OptionModuleProps {
|
||||
onChange: (item: any) => void;
|
||||
options: OptionsProps;
|
||||
}
|
||||
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
export const defaultProps = {
|
||||
options: {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
},
|
||||
};
|
||||
|
||||
interface Props extends PanelProps<OptionsProps> {}
|
||||
|
||||
class GaugePanel extends PureComponent<Props> {
|
||||
render() {
|
||||
const { timeSeries, width, height } = this.props;
|
||||
const { unit } = this.props.options;
|
||||
|
||||
const vmSeries = getTimeSeriesVMs({
|
||||
timeSeries: timeSeries,
|
||||
nullValueMode: NullValueMode.Ignore,
|
||||
});
|
||||
|
||||
return (
|
||||
<Gauge
|
||||
maxValue={100}
|
||||
minValue={0}
|
||||
timeSeries={vmSeries}
|
||||
thresholds={[0, 100]}
|
||||
height={height}
|
||||
width={width}
|
||||
unit={unit}
|
||||
/>
|
||||
);
|
||||
return <Gauge timeSeries={vmSeries} {...this.props.options} width={width} height={height} />;
|
||||
}
|
||||
}
|
||||
|
||||
export class GaugeOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
onUnitChange = value => {
|
||||
this.props.onChange({ ...this.props.options, unit: value });
|
||||
};
|
||||
class Options extends PureComponent<PanelOptionsProps<OptionsProps>> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Value</h5>
|
||||
<div className="gf-form-inline">
|
||||
<Label width={5}>Unit</Label>
|
||||
<UnitPicker defaultValue={this.props.options.unit.value} onSelected={value => this.onUnitChange(value)} />
|
||||
</div>
|
||||
</div>
|
||||
<ValueOptions onChange={this.props.onChange} options={this.props.options} />
|
||||
<GaugeOptions onChange={this.props.onChange} options={this.props.options} />
|
||||
<Thresholds onChange={this.props.onChange} options={this.props.options} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { GaugePanel as Panel, GaugeOptions as PanelOptions };
|
||||
export { GaugePanel as Panel, Options as PanelOptions, defaultProps as PanelDefaults };
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
DataQueryResponse,
|
||||
DataQueryOptions,
|
||||
} from './series';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
import { PanelProps, PanelOptionsProps, Threshold } from './panel';
|
||||
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
|
||||
import { Organization, OrganizationState } from './organization';
|
||||
import {
|
||||
@ -89,6 +89,7 @@ export {
|
||||
AppNotificationTimeout,
|
||||
DashboardSearchHit,
|
||||
UserState,
|
||||
Threshold,
|
||||
ValidationEvents,
|
||||
ValidationRule,
|
||||
};
|
||||
|
@ -28,3 +28,11 @@ export interface PanelMenuItem {
|
||||
shortcut?: string;
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
||||
export interface Threshold {
|
||||
index: number;
|
||||
label: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
canRemove: boolean;
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ export interface PluginExports {
|
||||
PanelCtrl?;
|
||||
Panel?: ComponentClass<PanelProps>;
|
||||
PanelOptions?: ComponentClass<PanelOptionsProps>;
|
||||
PanelDefaults?: any;
|
||||
}
|
||||
|
||||
export interface PanelPlugin {
|
||||
|
@ -1,86 +1,87 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { TimeSeriesVMs } from 'app/types';
|
||||
import { Threshold, TimeSeriesVMs } from 'app/types';
|
||||
import config from '../core/config';
|
||||
import kbn from '../core/utils/kbn';
|
||||
|
||||
interface Props {
|
||||
decimals: number;
|
||||
timeSeries: TimeSeriesVMs;
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
showThresholdMarkers?: boolean;
|
||||
thresholds?: number[];
|
||||
showThresholdLables?: boolean;
|
||||
unit: { label: string; value: string };
|
||||
showThresholdMarkers: boolean;
|
||||
thresholds: Threshold[];
|
||||
showThresholdLabels: boolean;
|
||||
unit: string;
|
||||
width: number;
|
||||
height: number;
|
||||
stat: string;
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
}
|
||||
|
||||
const colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
|
||||
|
||||
export class Gauge extends PureComponent<Props> {
|
||||
parentElement: any;
|
||||
canvasElement: any;
|
||||
|
||||
static defaultProps = {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLables: false,
|
||||
thresholds: [],
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
unit: 'none',
|
||||
thresholds: [
|
||||
{ label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
|
||||
{ label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
|
||||
],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
componentDidUpdate() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
formatValue(value) {
|
||||
const { unit } = this.props;
|
||||
const { decimals, prefix, suffix, unit } = this.props;
|
||||
|
||||
const formatFunc = kbn.valueFormats[unit.value];
|
||||
return formatFunc(value);
|
||||
const formatFunc = kbn.valueFormats[unit];
|
||||
|
||||
if (isNaN(value)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return `${prefix} ${formatFunc(value, decimals)} ${suffix}`;
|
||||
}
|
||||
|
||||
draw() {
|
||||
const {
|
||||
maxValue,
|
||||
minValue,
|
||||
showThresholdLables,
|
||||
showThresholdMarkers,
|
||||
timeSeries,
|
||||
thresholds,
|
||||
width,
|
||||
height,
|
||||
} = this.props;
|
||||
const { timeSeries, showThresholdLabels, showThresholdMarkers, thresholds, width, height, stat } = this.props;
|
||||
|
||||
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 gaugeWidth = Math.min(dimension / 6, 60);
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const formattedThresholds = [];
|
||||
|
||||
thresholds.forEach((threshold, index) => {
|
||||
formattedThresholds.push({
|
||||
value: threshold,
|
||||
color: colors[index],
|
||||
});
|
||||
const formattedThresholds = 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,
|
||||
};
|
||||
});
|
||||
|
||||
const options = {
|
||||
series: {
|
||||
gauges: {
|
||||
gauge: {
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
min: thresholds[0].value,
|
||||
max: thresholds[thresholds.length - 1].value,
|
||||
background: { color: backgroundColor },
|
||||
border: { color: null },
|
||||
shadow: { show: false },
|
||||
@ -93,7 +94,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
threshold: {
|
||||
values: formattedThresholds,
|
||||
label: {
|
||||
show: showThresholdLables,
|
||||
show: showThresholdLabels,
|
||||
margin: thresholdMarkersWidth + 1,
|
||||
font: { size: thresholdLabelFontSize },
|
||||
},
|
||||
@ -103,7 +104,11 @@ export class Gauge extends PureComponent<Props> {
|
||||
value: {
|
||||
color: fontColor,
|
||||
formatter: () => {
|
||||
return this.formatValue(timeSeries[0].stats.avg);
|
||||
if (timeSeries[0]) {
|
||||
return this.formatValue(timeSeries[0].stats[stat]);
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
font: {
|
||||
size: fontSize,
|
||||
@ -117,7 +122,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
|
||||
let value: string | number = 'N/A';
|
||||
if (timeSeries.length) {
|
||||
value = timeSeries[0].stats.avg;
|
||||
value = timeSeries[0].stats[stat];
|
||||
}
|
||||
|
||||
const plotSeries = {
|
||||
@ -135,7 +140,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
const { height, width } = this.props;
|
||||
|
||||
return (
|
||||
<div className="singlestat-panel" ref={element => (this.parentElement = element)}>
|
||||
<div className="singlestat-panel">
|
||||
<div
|
||||
style={{
|
||||
height: `${height * 0.9}px`,
|
||||
|
@ -103,6 +103,7 @@
|
||||
@import 'components/add_data_source.scss';
|
||||
@import 'components/page_loader';
|
||||
@import 'components/unit-picker';
|
||||
@import 'components/thresholds';
|
||||
|
||||
// PAGES
|
||||
@import 'pages/login';
|
||||
|
107
public/sass/components/_thresholds.scss
Normal file
107
public/sass/components/_thresholds.scss
Normal file
@ -0,0 +1,107 @@
|
||||
.thresholds {
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.threshold-rows {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.threshold-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
|
||||
&::before {
|
||||
font-family: 'FontAwesome';
|
||||
content: '\f0d9';
|
||||
color: $input-label-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-row-inner {
|
||||
border: 1px solid $input-label-border-color;
|
||||
border-radius: $border-radius;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 300px;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
.threshold-row-color {
|
||||
width: 36px;
|
||||
border-right: 1px solid $input-label-border-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
|
||||
.threshold-row-color-inner {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.threshold-row-input {
|
||||
padding: 8px 10px;
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
.threshold-row-label {
|
||||
background-color: $input-label-bg;
|
||||
padding: 5px;
|
||||
width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.threshold-row-add-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.threshold-row-min {
|
||||
margin-top: -22px;
|
||||
}
|
||||
|
||||
.threshold-row-max {
|
||||
margin-bottom: -22px;
|
||||
}
|
||||
|
||||
.threshold-row-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
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%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-indicators {
|
||||
width: 15px;
|
||||
border-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
}
|
1
public/vendor/flot/jquery.flot.gauge.js
vendored
1
public/vendor/flot/jquery.flot.gauge.js
vendored
@ -588,6 +588,7 @@
|
||||
if (!exists) {
|
||||
span = $("<span></span>")
|
||||
span.attr("id", id);
|
||||
span.attr("class", "flot-temp-elem");
|
||||
placeholder.append(span);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user