Unverified Commit c2aa6459 authored by Johannes Schill's avatar Johannes Schill Committed by GitHub
Browse files

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

Gauge value options
parents 33dd880e f77c3543
......@@ -6,6 +6,7 @@ interface Props {
for?: string;
children: ReactNode;
width?: number;
className?: string;
}
export const Label: SFC<Props> = props => {
......
......@@ -14,6 +14,16 @@ export default class UnitGroup extends PureComponent<ExtendedGroupProps, State>
expanded: false,
};
componentDidMount() {
if (this.props.selectProps) {
const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
if (value && this.props.options.some(option => option.value === value)) {
this.setState({ expanded: true });
}
}
}
componentDidUpdate(nextProps) {
if (nextProps.selectProps.inputValue !== '') {
this.setState({ expanded: true });
......
......@@ -8,11 +8,16 @@ import kbn from '../../../utils/kbn';
interface Props {
onSelected: (item: any) => {} | void;
defaultValue?: string;
width?: number;
}
export default class UnitPicker extends PureComponent<Props> {
static defaultProps = {
width: 12,
};
render() {
const { defaultValue, onSelected } = this.props;
const { defaultValue, onSelected, width } = this.props;
const unitGroups = kbn.getUnitFormats();
......@@ -42,6 +47,13 @@ export default class UnitPicker extends PureComponent<Props> {
overflowY: 'auto',
position: 'relative',
} as React.CSSProperties),
valueContainer: () =>
({
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '90px',
whiteSpace: 'nowrap',
} as React.CSSProperties),
};
const value = groupOptions.map(group => {
......@@ -51,7 +63,7 @@ export default class UnitPicker extends PureComponent<Props> {
return (
<Select
classNamePrefix="gf-form-select-box"
className="width-20 gf-form-input--form-dropdown"
className={`width-${width} gf-form-input--form-dropdown`}
defaultValue={value}
isSearchable={true}
menuShouldScrollIntoView={false}
......
import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import Drop from 'tether-drop';
import { ColorPickerPopover } from './ColorPickerPopover';
import { react2AngularDirective } from 'app/core/utils/react2angular';
......@@ -11,29 +10,17 @@ export interface Props {
}
export class ColorPicker extends React.Component<Props, any> {
pickerElem: any;
pickerElem: HTMLElement;
colorPickerDrop: any;
constructor(props) {
super(props);
this.openColorPicker = this.openColorPicker.bind(this);
this.closeColorPicker = this.closeColorPicker.bind(this);
this.setPickerElem = this.setPickerElem.bind(this);
this.onColorSelect = this.onColorSelect.bind(this);
}
setPickerElem(elem) {
this.pickerElem = $(elem);
}
openColorPicker() {
openColorPicker = () => {
const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
const dropContentElem = document.createElement('div');
ReactDOM.render(dropContent, dropContentElem);
const drop = new Drop({
target: this.pickerElem[0],
target: this.pickerElem,
content: dropContentElem,
position: 'top center',
classes: 'drop-popover',
......@@ -48,23 +35,23 @@ export class ColorPicker extends React.Component<Props, any> {
this.colorPickerDrop = drop;
this.colorPickerDrop.open();
}
};
closeColorPicker() {
closeColorPicker = () => {
setTimeout(() => {
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
this.colorPickerDrop.destroy();
}
}, 100);
}
};
onColorSelect(color) {
onColorSelect = color => {
this.props.onChange(color);
}
};
render() {
return (
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={this.setPickerElem}>
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={element => (this.pickerElem = element)}>
<div className="sp-preview">
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
</div>
......
......@@ -129,7 +129,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
const { dashboard, panel } = this.props;
const { plugin } = this.state;
return <PanelChrome component={plugin.exports.Panel} panel={panel} dashboard={dashboard} />;
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} />;
}
renderAngularPanel() {
......
// Libraries
import React, { ComponentClass, PureComponent } from 'react';
import React, { PureComponent } from 'react';
import { AutoSizer } from 'react-virtualized';
// Services
......@@ -16,12 +16,12 @@ import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
// Types
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { TimeRange, PanelProps } from 'app/types';
import { PanelPlugin, TimeRange } from 'app/types';
export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
component: ComponentClass<PanelProps>;
plugin: PanelPlugin;
}
export interface State {
......@@ -80,11 +80,11 @@ export class PanelChrome extends PureComponent<Props, State> {
}
render() {
const { panel, dashboard } = this.props;
const { panel, dashboard, plugin } = this.props;
const { refreshCounter, timeRange, timeInfo, renderCounter } = this.state;
const { datasource, targets } = panel;
const PanelComponent = this.props.component;
const PanelComponent = plugin.exports.Panel;
return (
<AutoSizer>
......@@ -111,7 +111,7 @@ export class PanelChrome extends PureComponent<Props, State> {
loading={loading}
timeSeries={timeSeries}
timeRange={timeRange}
options={panel.getOptions()}
options={panel.getOptions(plugin.exports.PanelDefaults)}
width={width}
height={height - PANEL_HEADER_HEIGHT}
renderCounter={renderCounter}
......
......@@ -25,12 +25,18 @@ export class VisualizationTab extends PureComponent<Props> {
element: HTMLElement;
angularOptions: AngularComponent;
constructor(props) {
super(props);
}
getPanelDefaultOptions = () => {
const { panel, plugin } = this.props;
if (plugin.exports.PanelDefaults) {
return panel.getOptions(plugin.exports.PanelDefaults.options);
}
return panel.getOptions(plugin.exports.PanelDefaults);
};
renderPanelOptions() {
const { plugin, panel, angularPanel } = this.props;
const { plugin, angularPanel } = this.props;
const { PanelOptions } = plugin.exports;
if (angularPanel) {
......@@ -38,7 +44,7 @@ export class VisualizationTab extends PureComponent<Props> {
}
if (PanelOptions) {
return <PanelOptions options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
return <PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />;
} else {
return <p>Visualization has no options</p>;
}
......
......@@ -108,8 +108,8 @@ export class PanelModel {
_.defaultsDeep(this, _.cloneDeep(defaults));
}
getOptions() {
return this[this.getOptionsKey()] || {};
getOptions(panelDefaults) {
return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
}
updateOptions(options: object) {
......
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>
);
......
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 },
]);
});
});
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>,
];
}