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;
|
for?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Label: SFC<Props> = props => {
|
export const Label: SFC<Props> = props => {
|
||||||
|
@ -14,6 +14,16 @@ export default class UnitGroup extends PureComponent<ExtendedGroupProps, State>
|
|||||||
expanded: false,
|
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) {
|
componentDidUpdate(nextProps) {
|
||||||
if (nextProps.selectProps.inputValue !== '') {
|
if (nextProps.selectProps.inputValue !== '') {
|
||||||
this.setState({ expanded: true });
|
this.setState({ expanded: true });
|
||||||
|
@ -8,11 +8,16 @@ import kbn from '../../../utils/kbn';
|
|||||||
interface Props {
|
interface Props {
|
||||||
onSelected: (item: any) => {} | void;
|
onSelected: (item: any) => {} | void;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
|
width?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class UnitPicker extends PureComponent<Props> {
|
export default class UnitPicker extends PureComponent<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
width: 12,
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { defaultValue, onSelected } = this.props;
|
const { defaultValue, onSelected, width } = this.props;
|
||||||
|
|
||||||
const unitGroups = kbn.getUnitFormats();
|
const unitGroups = kbn.getUnitFormats();
|
||||||
|
|
||||||
@ -42,6 +47,13 @@ export default class UnitPicker extends PureComponent<Props> {
|
|||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
} as React.CSSProperties),
|
} as React.CSSProperties),
|
||||||
|
valueContainer: () =>
|
||||||
|
({
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
maxWidth: '90px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
} as React.CSSProperties),
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = groupOptions.map(group => {
|
const value = groupOptions.map(group => {
|
||||||
@ -51,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-20 gf-form-input--form-dropdown"
|
className={`width-${width} gf-form-input--form-dropdown`}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
menuShouldScrollIntoView={false}
|
menuShouldScrollIntoView={false}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import $ from 'jquery';
|
|
||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||||
@ -11,29 +10,17 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ColorPicker extends React.Component<Props, any> {
|
export class ColorPicker extends React.Component<Props, any> {
|
||||||
pickerElem: any;
|
pickerElem: HTMLElement;
|
||||||
colorPickerDrop: any;
|
colorPickerDrop: any;
|
||||||
|
|
||||||
constructor(props) {
|
openColorPicker = () => {
|
||||||
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() {
|
|
||||||
const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
|
const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
|
||||||
|
|
||||||
const dropContentElem = document.createElement('div');
|
const dropContentElem = document.createElement('div');
|
||||||
ReactDOM.render(dropContent, dropContentElem);
|
ReactDOM.render(dropContent, dropContentElem);
|
||||||
|
|
||||||
const drop = new Drop({
|
const drop = new Drop({
|
||||||
target: this.pickerElem[0],
|
target: this.pickerElem,
|
||||||
content: dropContentElem,
|
content: dropContentElem,
|
||||||
position: 'top center',
|
position: 'top center',
|
||||||
classes: 'drop-popover',
|
classes: 'drop-popover',
|
||||||
@ -48,23 +35,23 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
|
|
||||||
this.colorPickerDrop = drop;
|
this.colorPickerDrop = drop;
|
||||||
this.colorPickerDrop.open();
|
this.colorPickerDrop.open();
|
||||||
}
|
};
|
||||||
|
|
||||||
closeColorPicker() {
|
closeColorPicker = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
|
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
|
||||||
this.colorPickerDrop.destroy();
|
this.colorPickerDrop.destroy();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
};
|
||||||
|
|
||||||
onColorSelect(color) {
|
onColorSelect = color => {
|
||||||
this.props.onChange(color);
|
this.props.onChange(color);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
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">
|
||||||
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
|
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,7 +129,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
|||||||
const { dashboard, panel } = this.props;
|
const { dashboard, panel } = this.props;
|
||||||
const { plugin } = this.state;
|
const { plugin } = this.state;
|
||||||
|
|
||||||
return <PanelChrome component={plugin.exports.Panel} panel={panel} dashboard={dashboard} />;
|
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAngularPanel() {
|
renderAngularPanel() {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { ComponentClass, PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { AutoSizer } from 'react-virtualized';
|
import { AutoSizer } from 'react-virtualized';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
@ -16,12 +16,12 @@ import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
|
|||||||
// Types
|
// Types
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import { DashboardModel } from '../dashboard_model';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import { TimeRange, PanelProps } from 'app/types';
|
import { PanelPlugin, TimeRange } from 'app/types';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
component: ComponentClass<PanelProps>;
|
plugin: PanelPlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
@ -80,11 +80,11 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { panel, dashboard } = this.props;
|
const { panel, dashboard, plugin } = this.props;
|
||||||
const { refreshCounter, timeRange, timeInfo, renderCounter } = this.state;
|
const { refreshCounter, timeRange, timeInfo, renderCounter } = this.state;
|
||||||
|
|
||||||
const { datasource, targets } = panel;
|
const { datasource, targets } = panel;
|
||||||
const PanelComponent = this.props.component;
|
const PanelComponent = plugin.exports.Panel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
@ -111,7 +111,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
timeSeries={timeSeries}
|
timeSeries={timeSeries}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
options={panel.getOptions()}
|
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
||||||
width={width}
|
width={width}
|
||||||
height={height - PANEL_HEADER_HEIGHT}
|
height={height - PANEL_HEADER_HEIGHT}
|
||||||
renderCounter={renderCounter}
|
renderCounter={renderCounter}
|
||||||
|
@ -25,12 +25,18 @@ export class VisualizationTab extends PureComponent<Props> {
|
|||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
angularOptions: AngularComponent;
|
angularOptions: AngularComponent;
|
||||||
|
|
||||||
constructor(props) {
|
getPanelDefaultOptions = () => {
|
||||||
super(props);
|
const { panel, plugin } = this.props;
|
||||||
}
|
|
||||||
|
if (plugin.exports.PanelDefaults) {
|
||||||
|
return panel.getOptions(plugin.exports.PanelDefaults.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return panel.getOptions(plugin.exports.PanelDefaults);
|
||||||
|
};
|
||||||
|
|
||||||
renderPanelOptions() {
|
renderPanelOptions() {
|
||||||
const { plugin, panel, angularPanel } = this.props;
|
const { plugin, angularPanel } = this.props;
|
||||||
const { PanelOptions } = plugin.exports;
|
const { PanelOptions } = plugin.exports;
|
||||||
|
|
||||||
if (angularPanel) {
|
if (angularPanel) {
|
||||||
@ -38,7 +44,7 @@ export class VisualizationTab extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (PanelOptions) {
|
if (PanelOptions) {
|
||||||
return <PanelOptions options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
|
return <PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||||
} else {
|
} else {
|
||||||
return <p>Visualization has no options</p>;
|
return <p>Visualization has no options</p>;
|
||||||
}
|
}
|
||||||
|
@ -108,8 +108,8 @@ export class PanelModel {
|
|||||||
_.defaultsDeep(this, _.cloneDeep(defaults));
|
_.defaultsDeep(this, _.cloneDeep(defaults));
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptions() {
|
getOptions(panelDefaults) {
|
||||||
return this[this.getOptionsKey()] || {};
|
return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOptions(options: object) {
|
updateOptions(options: object) {
|
||||||
|
@ -1,14 +1,35 @@
|
|||||||
import React, { PureComponent } from 'react';
|
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() {
|
render() {
|
||||||
|
const { showThresholdLabels, showThresholdMarkers } = this.props.options;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="section gf-form-group">
|
||||||
<div className="section gf-form-group">
|
<h5 className="page-heading">Gauge</h5>
|
||||||
<h5 className="page-heading">Draw Modes</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>
|
||||||
</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 React, { PureComponent } from 'react';
|
||||||
import Gauge from 'app/viz/Gauge';
|
import Gauge from 'app/viz/Gauge';
|
||||||
import { Label } from 'app/core/components/Label/Label';
|
import { NullValueMode, PanelOptionsProps, PanelProps, Threshold } from 'app/types';
|
||||||
import UnitPicker from 'app/core/components/Picker/Unit/UnitPicker';
|
|
||||||
import { NullValueMode, PanelOptionsProps, PanelProps } from 'app/types';
|
|
||||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||||
|
import ValueOptions from './ValueOptions';
|
||||||
|
import GaugeOptions from './GaugeOptions';
|
||||||
|
import Thresholds from './Thresholds';
|
||||||
|
|
||||||
export interface Options {
|
export interface OptionsProps {
|
||||||
unit: { label: string; value: string };
|
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() {
|
render() {
|
||||||
const { timeSeries, width, height } = this.props;
|
const { timeSeries, width, height } = this.props;
|
||||||
const { unit } = this.props.options;
|
|
||||||
|
|
||||||
const vmSeries = getTimeSeriesVMs({
|
const vmSeries = getTimeSeriesVMs({
|
||||||
timeSeries: timeSeries,
|
timeSeries: timeSeries,
|
||||||
nullValueMode: NullValueMode.Ignore,
|
nullValueMode: NullValueMode.Ignore,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return <Gauge timeSeries={vmSeries} {...this.props.options} width={width} height={height} />;
|
||||||
<Gauge
|
|
||||||
maxValue={100}
|
|
||||||
minValue={0}
|
|
||||||
timeSeries={vmSeries}
|
|
||||||
thresholds={[0, 100]}
|
|
||||||
height={height}
|
|
||||||
width={width}
|
|
||||||
unit={unit}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GaugeOptions extends PureComponent<PanelOptionsProps<Options>> {
|
class Options extends PureComponent<PanelOptionsProps<OptionsProps>> {
|
||||||
onUnitChange = value => {
|
static defaultProps = defaultProps;
|
||||||
this.props.onChange({ ...this.props.options, unit: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="section gf-form-group">
|
<ValueOptions onChange={this.props.onChange} options={this.props.options} />
|
||||||
<h5 className="page-heading">Value</h5>
|
<GaugeOptions onChange={this.props.onChange} options={this.props.options} />
|
||||||
<div className="gf-form-inline">
|
<Thresholds onChange={this.props.onChange} options={this.props.options} />
|
||||||
<Label width={5}>Unit</Label>
|
|
||||||
<UnitPicker defaultValue={this.props.options.unit.value} onSelected={value => this.onUnitChange(value)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { GaugePanel as Panel, GaugeOptions as PanelOptions };
|
export { GaugePanel as Panel, Options as PanelOptions, defaultProps as PanelDefaults };
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
DataQueryOptions,
|
DataQueryOptions,
|
||||||
} from './series';
|
} from './series';
|
||||||
import { PanelProps, PanelOptionsProps } from './panel';
|
import { PanelProps, PanelOptionsProps, Threshold } 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 {
|
||||||
@ -89,6 +89,7 @@ export {
|
|||||||
AppNotificationTimeout,
|
AppNotificationTimeout,
|
||||||
DashboardSearchHit,
|
DashboardSearchHit,
|
||||||
UserState,
|
UserState,
|
||||||
|
Threshold,
|
||||||
ValidationEvents,
|
ValidationEvents,
|
||||||
ValidationRule,
|
ValidationRule,
|
||||||
};
|
};
|
||||||
|
@ -28,3 +28,11 @@ export interface PanelMenuItem {
|
|||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
subMenu?: PanelMenuItem[];
|
subMenu?: PanelMenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Threshold {
|
||||||
|
index: number;
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
color?: string;
|
||||||
|
canRemove: boolean;
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ export interface PluginExports {
|
|||||||
PanelCtrl?;
|
PanelCtrl?;
|
||||||
Panel?: ComponentClass<PanelProps>;
|
Panel?: ComponentClass<PanelProps>;
|
||||||
PanelOptions?: ComponentClass<PanelOptionsProps>;
|
PanelOptions?: ComponentClass<PanelOptionsProps>;
|
||||||
|
PanelDefaults?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelPlugin {
|
export interface PanelPlugin {
|
||||||
|
@ -1,86 +1,87 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { TimeSeriesVMs } from 'app/types';
|
import { Threshold, TimeSeriesVMs } 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;
|
||||||
timeSeries: TimeSeriesVMs;
|
timeSeries: TimeSeriesVMs;
|
||||||
minValue: number;
|
showThresholdMarkers: boolean;
|
||||||
maxValue: number;
|
thresholds: Threshold[];
|
||||||
showThresholdMarkers?: boolean;
|
showThresholdLabels: boolean;
|
||||||
thresholds?: number[];
|
unit: string;
|
||||||
showThresholdLables?: boolean;
|
|
||||||
unit: { label: string; value: string };
|
|
||||||
width: number;
|
width: number;
|
||||||
height: 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> {
|
export class Gauge extends PureComponent<Props> {
|
||||||
parentElement: any;
|
|
||||||
canvasElement: any;
|
canvasElement: any;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
|
prefix: '',
|
||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
showThresholdLables: false,
|
showThresholdLabels: false,
|
||||||
thresholds: [],
|
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() {
|
componentDidMount() {
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate() {
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
formatValue(value) {
|
formatValue(value) {
|
||||||
const { unit } = this.props;
|
const { decimals, prefix, suffix, unit } = this.props;
|
||||||
|
|
||||||
const formatFunc = kbn.valueFormats[unit.value];
|
const formatFunc = kbn.valueFormats[unit];
|
||||||
return formatFunc(value);
|
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix} ${formatFunc(value, decimals)} ${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
const {
|
const { timeSeries, showThresholdLabels, showThresholdMarkers, thresholds, width, height, stat } = this.props;
|
||||||
maxValue,
|
|
||||||
minValue,
|
|
||||||
showThresholdLables,
|
|
||||||
showThresholdMarkers,
|
|
||||||
timeSeries,
|
|
||||||
thresholds,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const dimension = Math.min(width, height * 1.3);
|
const dimension = Math.min(width, height * 1.3);
|
||||||
const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
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 fontColor = config.bootData.user.lightTheme ? 'rgb(38,38,38)' : 'rgb(230,230,230)';
|
||||||
const fontScale = parseInt('80', 10) / 100;
|
const fontScale = parseInt('80', 10) / 100;
|
||||||
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
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 thresholdMarkersWidth = gaugeWidth / 5;
|
||||||
const thresholdLabelFontSize = fontSize / 2.5;
|
const thresholdLabelFontSize = fontSize / 2.5;
|
||||||
|
|
||||||
const formattedThresholds = [];
|
const formattedThresholds = thresholds.map((threshold, index) => {
|
||||||
|
return {
|
||||||
thresholds.forEach((threshold, index) => {
|
value: threshold.value,
|
||||||
formattedThresholds.push({
|
// Hacky way to get correct color for threshold.
|
||||||
value: threshold,
|
color: index === 0 ? threshold.color : thresholds[index - 1].color,
|
||||||
color: colors[index],
|
};
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
series: {
|
series: {
|
||||||
gauges: {
|
gauges: {
|
||||||
gauge: {
|
gauge: {
|
||||||
min: minValue,
|
min: thresholds[0].value,
|
||||||
max: maxValue,
|
max: thresholds[thresholds.length - 1].value,
|
||||||
background: { color: backgroundColor },
|
background: { color: backgroundColor },
|
||||||
border: { color: null },
|
border: { color: null },
|
||||||
shadow: { show: false },
|
shadow: { show: false },
|
||||||
@ -93,7 +94,7 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
threshold: {
|
threshold: {
|
||||||
values: formattedThresholds,
|
values: formattedThresholds,
|
||||||
label: {
|
label: {
|
||||||
show: showThresholdLables,
|
show: showThresholdLabels,
|
||||||
margin: thresholdMarkersWidth + 1,
|
margin: thresholdMarkersWidth + 1,
|
||||||
font: { size: thresholdLabelFontSize },
|
font: { size: thresholdLabelFontSize },
|
||||||
},
|
},
|
||||||
@ -103,7 +104,11 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
value: {
|
value: {
|
||||||
color: fontColor,
|
color: fontColor,
|
||||||
formatter: () => {
|
formatter: () => {
|
||||||
return this.formatValue(timeSeries[0].stats.avg);
|
if (timeSeries[0]) {
|
||||||
|
return this.formatValue(timeSeries[0].stats[stat]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
size: fontSize,
|
size: fontSize,
|
||||||
@ -117,7 +122,7 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
|
|
||||||
let value: string | number = 'N/A';
|
let value: string | number = 'N/A';
|
||||||
if (timeSeries.length) {
|
if (timeSeries.length) {
|
||||||
value = timeSeries[0].stats.avg;
|
value = timeSeries[0].stats[stat];
|
||||||
}
|
}
|
||||||
|
|
||||||
const plotSeries = {
|
const plotSeries = {
|
||||||
@ -135,7 +140,7 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
const { height, width } = this.props;
|
const { height, width } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="singlestat-panel" ref={element => (this.parentElement = element)}>
|
<div className="singlestat-panel">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: `${height * 0.9}px`,
|
height: `${height * 0.9}px`,
|
||||||
|
@ -103,6 +103,7 @@
|
|||||||
@import 'components/add_data_source.scss';
|
@import 'components/add_data_source.scss';
|
||||||
@import 'components/page_loader';
|
@import 'components/page_loader';
|
||||||
@import 'components/unit-picker';
|
@import 'components/unit-picker';
|
||||||
|
@import 'components/thresholds';
|
||||||
|
|
||||||
// PAGES
|
// PAGES
|
||||||
@import 'pages/login';
|
@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) {
|
if (!exists) {
|
||||||
span = $("<span></span>")
|
span = $("<span></span>")
|
||||||
span.attr("id", id);
|
span.attr("id", id);
|
||||||
|
span.attr("class", "flot-temp-elem");
|
||||||
placeholder.append(span);
|
placeholder.append(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user