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;
children: ReactNode;
width?: number;
className?: string;
}
export const Label: SFC<Props> = props => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -28,3 +28,11 @@ export interface PanelMenuItem {
shortcut?: string;
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?;
Panel?: ComponentClass<PanelProps>;
PanelOptions?: ComponentClass<PanelOptionsProps>;
PanelDefaults?: any;
}
export interface PanelPlugin {

View File

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

View File

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

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) {
span = $("<span></span>")
span.attr("id", id);
span.attr("class", "flot-temp-elem");
placeholder.append(span);
}