Merge pull request #14234 from grafana/gauge-value-options

Gauge value options
This commit is contained in:
Johannes Schill 2018-12-04 16:30:34 +01:00 committed by GitHub
commit c2aa64595a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 803 additions and 116 deletions

View File

@ -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 => {

View File

@ -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 });

View File

@ -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}

View File

@ -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>

View File

@ -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() {

View File

@ -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}

View File

@ -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>;
} }

View File

@ -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) {

View File

@ -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">Draw Modes</h5> <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>
</div> </div>
); );

View 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 },
]);
});
});

View 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>
);
}
}

View 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>
);
}
}

View File

@ -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 };

View File

@ -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,
}; };

View File

@ -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;
}

View File

@ -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 {

View File

@ -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`,

View File

@ -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';

View 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;
}

View File

@ -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);
} }