Browse Source

Merge pull request #14534 from grafana/14409/threshold-ux-changes

14409/threshold ux changes
Torkel Ödegaard 7 years ago
parent
commit
a4091813bf

+ 7 - 5
pkg/api/frontendsettings.go

@@ -186,16 +186,18 @@ func getPanelSort(id string) int {
 		sort = 1
 		sort = 1
 	case "singlestat":
 	case "singlestat":
 		sort = 2
 		sort = 2
-	case "table":
+	case "gauge":
 		sort = 3
 		sort = 3
-	case "text":
+	case "table":
 		sort = 4
 		sort = 4
-	case "heatmap":
+	case "text":
 		sort = 5
 		sort = 5
-	case "alertlist":
+	case "heatmap":
 		sort = 6
 		sort = 6
-	case "dashlist":
+	case "alertlist":
 		sort = 7
 		sort = 7
+	case "dashlist":
+		sort = 8
 	}
 	}
 	return sort
 	return sort
 }
 }

+ 1 - 1
public/app/core/components/Select/UnitPicker.tsx

@@ -3,7 +3,7 @@ import Select from './Select';
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
 
 
 interface Props {
 interface Props {
-  onChange: (item: any) => {} | void;
+  onChange: (item: any) => void;
   defaultValue?: string;
   defaultValue?: string;
   width?: number;
   width?: number;
 }
 }

+ 27 - 18
public/app/plugins/panel/gauge/GaugeOptions.tsx

@@ -1,36 +1,45 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import { Switch } from 'app/core/components/Switch/Switch';
 import { Switch } from 'app/core/components/Switch/Switch';
 import { OptionModuleProps } from './module';
 import { OptionModuleProps } from './module';
+import { Label } from '../../../core/components/Label/Label';
 
 
 export default class GaugeOptions extends PureComponent<OptionModuleProps> {
 export default class GaugeOptions extends PureComponent<OptionModuleProps> {
-  toggleThresholdLabels = () =>
+  onToggleThresholdLabels = () =>
     this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
     this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
 
 
-  toggleThresholdMarkers = () =>
+  onToggleThresholdMarkers = () =>
     this.props.onChange({ ...this.props.options, showThresholdMarkers: !this.props.options.showThresholdMarkers });
     this.props.onChange({ ...this.props.options, showThresholdMarkers: !this.props.options.showThresholdMarkers });
 
 
+  onMinValueChange = ({ target }) => this.props.onChange({ ...this.props.options, minValue: target.value });
+
+  onMaxValueChange = ({ target }) => this.props.onChange({ ...this.props.options, maxValue: target.value });
+
   render() {
   render() {
-    const { showThresholdLabels, showThresholdMarkers } = this.props.options;
+    const { maxValue, minValue, showThresholdLabels, showThresholdMarkers } = this.props.options;
 
 
     return (
     return (
       <div className="section gf-form-group">
       <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}
-          />
+        <h5 className="section-heading">Gauge</h5>
+        <div className="gf-form">
+          <Label width={8}>Min value</Label>
+          <input type="text" className="gf-form-input width-12" onChange={this.onMinValueChange} value={minValue} />
         </div>
         </div>
-        <div className="gf-form-inline">
-          <Switch
-            label="Threshold markers"
-            labelClass="width-10"
-            checked={showThresholdMarkers}
-            onChange={this.toggleThresholdMarkers}
-          />
+        <div className="gf-form">
+          <Label width={8}>Max value</Label>
+          <input type="text" className="gf-form-input width-12" onChange={this.onMaxValueChange} value={maxValue} />
         </div>
         </div>
+        <Switch
+          label="Show labels"
+          labelClass="width-8"
+          checked={showThresholdLabels}
+          onChange={this.onToggleThresholdLabels}
+        />
+        <Switch
+          label="Show markers"
+          labelClass="width-8"
+          checked={showThresholdMarkers}
+          onChange={this.onToggleThresholdMarkers}
+        />
       </div>
       </div>
     );
     );
   }
   }

+ 46 - 54
public/app/plugins/panel/gauge/MappingRow.tsx

@@ -62,69 +62,59 @@ export default class MappingRow extends PureComponent<Props, State> {
 
 
     if (type === MappingType.RangeToText) {
     if (type === MappingType.RangeToText) {
       return (
       return (
-        <div className="gf-form">
-          <div className="gf-form-inline mapping-row-input">
+        <>
+          <div className="gf-form">
             <Label width={4}>From</Label>
             <Label width={4}>From</Label>
-            <div>
-              <input
-                className="gf-form-input"
-                value={from}
-                onBlur={this.updateMapping}
-                onChange={this.onMappingFromChange}
-              />
-            </div>
+            <input
+              className="gf-form-input width-8"
+              value={from}
+              onBlur={this.updateMapping}
+              onChange={this.onMappingFromChange}
+            />
           </div>
           </div>
-          <div className="gf-form-inline mapping-row-input">
+          <div className="gf-form">
             <Label width={4}>To</Label>
             <Label width={4}>To</Label>
-            <div>
-              <input
-                className="gf-form-input"
-                value={to}
-                onBlur={this.updateMapping}
-                onChange={this.onMappingToChange}
-              />
-            </div>
+            <input
+              className="gf-form-input width-8"
+              value={to}
+              onBlur={this.updateMapping}
+              onChange={this.onMappingToChange}
+            />
           </div>
           </div>
-          <div className="gf-form-inline mapping-row-input">
+          <div className="gf-form">
             <Label width={4}>Text</Label>
             <Label width={4}>Text</Label>
-            <div>
-              <input
-                className="gf-form-input"
-                value={text}
-                onBlur={this.updateMapping}
-                onChange={this.onMappingTextChange}
-              />
-            </div>
+            <input
+              className="gf-form-input width-10"
+              value={text}
+              onBlur={this.updateMapping}
+              onChange={this.onMappingTextChange}
+            />
           </div>
           </div>
-        </div>
+        </>
       );
       );
     }
     }
 
 
     return (
     return (
-      <div className="gf-form">
-        <div className="gf-form-inline mapping-row-input">
+      <>
+        <div className="gf-form">
           <Label width={4}>Value</Label>
           <Label width={4}>Value</Label>
-          <div>
-            <input
-              className="gf-form-input"
-              onBlur={this.updateMapping}
-              onChange={this.onMappingValueChange}
-              value={value}
-            />
-          </div>
+          <input
+            className="gf-form-input width-8"
+            onBlur={this.updateMapping}
+            onChange={this.onMappingValueChange}
+            value={value}
+          />
         </div>
         </div>
-        <div className="gf-form-inline mapping-row-input">
+        <div className="gf-form gf-form--grow">
           <Label width={4}>Text</Label>
           <Label width={4}>Text</Label>
-          <div>
-            <input
-              className="gf-form-input"
-              onBlur={this.updateMapping}
-              value={text}
-              onChange={this.onMappingTextChange}
-            />
-          </div>
+          <input
+            className="gf-form-input"
+            onBlur={this.updateMapping}
+            value={text}
+            onChange={this.onMappingTextChange}
+          />
         </div>
         </div>
-      </div>
+      </>
     );
     );
   }
   }
 
 
@@ -132,8 +122,8 @@ export default class MappingRow extends PureComponent<Props, State> {
     const { type } = this.state;
     const { type } = this.state;
 
 
     return (
     return (
-      <div className="mapping-row">
-        <div className="gf-form-inline mapping-row-type">
+      <div className="gf-form-inline">
+        <div className="gf-form">
           <Label width={5}>Type</Label>
           <Label width={5}>Type</Label>
           <Select
           <Select
             placeholder="Choose type"
             placeholder="Choose type"
@@ -144,9 +134,11 @@ export default class MappingRow extends PureComponent<Props, State> {
             width={7}
             width={7}
           />
           />
         </div>
         </div>
-        <div>{this.renderRow()}</div>
-        <div onClick={this.props.removeMapping} className="threshold-row-remove">
-          <i className="fa fa-times" />
+        {this.renderRow()}
+        <div className="gf-form">
+          <button onClick={this.props.removeMapping} className="gf-form-label gf-form-label--btn">
+            <i className="fa fa-times" />
+          </button>
         </div>
         </div>
       </div>
       </div>
     );
     );

+ 15 - 88
public/app/plugins/panel/gauge/Threshold.test.tsx

@@ -2,17 +2,14 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 import Thresholds from './Thresholds';
 import Thresholds from './Thresholds';
 import { defaultProps, OptionsProps } from './module';
 import { defaultProps, OptionsProps } from './module';
-import { PanelOptionsProps } from '../../../types';
+import { BasicGaugeColor, PanelOptionsProps } from 'app/types';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: PanelOptionsProps<OptionsProps> = {
   const props: PanelOptionsProps<OptionsProps> = {
     onChange: jest.fn(),
     onChange: jest.fn(),
     options: {
     options: {
       ...defaultProps.options,
       ...defaultProps.options,
-      thresholds: [
-        { index: 0, label: 'Min', value: 0, canRemove: false, color: 'rgba(50, 172, 45, 0.97)' },
-        { index: 1, label: 'Max', value: 100, canRemove: false },
-      ],
+      thresholds: [],
     },
     },
   };
   };
 
 
@@ -22,121 +19,51 @@ const setup = (propOverrides?: object) => {
 };
 };
 
 
 describe('Add threshold', () => {
 describe('Add threshold', () => {
-  it('should add threshold between min and max', () => {
+  it('should add threshold', () => {
     const instance = setup();
     const instance = setup();
 
 
-    instance.onAddThreshold(1);
+    instance.onAddThreshold(0);
 
 
-    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 },
-    ]);
+    expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
   });
   });
 
 
-  it('should add threshold between min and added threshold', () => {
+  it('should add another threshold above a first', () => {
     const instance = setup({
     const instance = setup({
       options: {
       options: {
         ...defaultProps.options,
         ...defaultProps.options,
-        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 },
-        ],
+        thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
       },
       },
     });
     });
 
 
     instance.onAddThreshold(1);
     instance.onAddThreshold(1);
 
 
     expect(instance.state.thresholds).toEqual([
     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 },
+      { index: 1, value: 75, color: 'rgb(170, 95, 61)' },
+      { index: 0, value: 50, color: 'rgb(127, 115, 64)' },
     ]);
     ]);
   });
   });
 });
 });
 
 
-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', () => {
 describe('change threshold value', () => {
   it('should update value and resort rows', () => {
   it('should update value and resort rows', () => {
     const instance = setup();
     const instance = setup();
     const mockThresholds = [
     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 },
+      { index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
+      { index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
     ];
     ];
 
 
     instance.state = {
     instance.state = {
+      baseColor: BasicGaugeColor.Green,
       thresholds: mockThresholds,
       thresholds: mockThresholds,
     };
     };
 
 
     const mockEvent = { target: { value: 78 } };
     const mockEvent = { target: { value: 78 } };
 
 
-    instance.onChangeThresholdValue(mockEvent, mockThresholds[1]);
+    instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
 
 
     expect(instance.state.thresholds).toEqual([
     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 },
+      { index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
+      { index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
     ]);
     ]);
   });
   });
 });
 });

+ 69 - 137
public/app/plugins/panel/gauge/Thresholds.tsx

@@ -1,11 +1,12 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import classNames from 'classnames/bind';
+import tinycolor from 'tinycolor2';
 import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
 import { ColorPicker } from 'app/core/components/colorpicker/ColorPicker';
 import { OptionModuleProps } from './module';
 import { OptionModuleProps } from './module';
 import { BasicGaugeColor, Threshold } from 'app/types';
 import { BasicGaugeColor, Threshold } from 'app/types';
 
 
 interface State {
 interface State {
   thresholds: Threshold[];
   thresholds: Threshold[];
+  baseColor: string;
 }
 }
 
 
 export default class Thresholds extends PureComponent<OptionModuleProps, State> {
 export default class Thresholds extends PureComponent<OptionModuleProps, State> {
@@ -14,10 +15,12 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
 
 
     this.state = {
     this.state = {
       thresholds: props.options.thresholds,
       thresholds: props.options.thresholds,
+      baseColor: props.options.baseColor,
     };
     };
   }
   }
 
 
   onAddThreshold = index => {
   onAddThreshold = index => {
+    const { maxValue, minValue } = this.props.options;
     const { thresholds } = this.state;
     const { thresholds } = this.state;
 
 
     const newThresholds = thresholds.map(threshold => {
     const newThresholds = thresholds.map(threshold => {
@@ -28,15 +31,28 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
       return threshold;
       return threshold;
     });
     });
 
 
-    // Setting value to a value between the new threshold.
-    const value = newThresholds[index].value - (newThresholds[index].value - newThresholds[index - 1].value) / 2;
+    // Setting value to a value between the previous thresholds
+    let value;
+
+    if (index === 0 && thresholds.length === 0) {
+      value = maxValue - (maxValue - minValue) / 2;
+    } else if (index === 0 && thresholds.length > 0) {
+      value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2;
+    } else if (index > newThresholds[newThresholds.length - 1].index) {
+      value = maxValue - (maxValue - newThresholds[index - 1].value) / 2;
+    }
+
+    // Set a color that lies between the previous thresholds
+    let color;
+    if (index === 0 && thresholds.length === 0) {
+      color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
+    } else {
+      color = tinycolor.mix(thresholds[index - 1].color, BasicGaugeColor.Red, 50).toRgbString();
+    }
 
 
     this.setState(
     this.setState(
       {
       {
-        thresholds: this.sortThresholds([
-          ...newThresholds,
-          { index: index, label: '', value: value, canRemove: true, color: BasicGaugeColor.Orange },
-        ]),
+        thresholds: this.sortThresholds([...newThresholds, { index: index, value: value, color: color }]),
       },
       },
       () => this.updateGauge()
       () => this.updateGauge()
     );
     );
@@ -86,6 +102,7 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
     );
     );
   };
   };
 
 
+  onChangeBaseColor = color => this.props.onChange({ ...this.props.options, baseColor: color });
   onBlur = () => {
   onBlur = () => {
     this.setState(prevState => ({
     this.setState(prevState => ({
       thresholds: this.sortThresholds(prevState.thresholds),
       thresholds: this.sortThresholds(prevState.thresholds),
@@ -100,78 +117,16 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
 
 
   sortThresholds = thresholds => {
   sortThresholds = thresholds => {
     return thresholds.sort((t1, t2) => {
     return thresholds.sort((t1, t2) => {
-      return t1.value - t2.value;
+      return t2.value - t1.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() {
   renderThresholds() {
     const { thresholds } = this.state;
     const { thresholds } = this.state;
 
 
     return thresholds.map((threshold, index) => {
     return thresholds.map((threshold, index) => {
-      const rowStyle = classNames({
-        'threshold-row': true,
-        'threshold-row-min': index === 0,
-        'threshold-row-max': index === thresholds.length - 1,
-      });
-
       return (
       return (
-        <div className={rowStyle} key={`${threshold.index}-${index}`}>
+        <div className="threshold-row" key={`${threshold.index}-${index}`}>
           <div className="threshold-row-inner">
           <div className="threshold-row-inner">
             <div className="threshold-row-color">
             <div className="threshold-row-color">
               {threshold.color && (
               {threshold.color && (
@@ -190,103 +145,80 @@ export default class Thresholds extends PureComponent<OptionModuleProps, State>
               value={threshold.value}
               value={threshold.value}
               onBlur={this.onBlur}
               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 onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
+              <i className="fa fa-times" />
+            </div>
           </div>
           </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) {
+  renderIndicator() {
     const { thresholds } = this.state;
     const { thresholds } = this.state;
-    const indicators = thresholds.length - 1;
 
 
-    if (index === 0 || index === thresholds.length) {
+    return thresholds.map((t, i) => {
       return (
       return (
-        <div
-          key={index}
-          className="indicator-section"
-          style={{
-            height: `calc(100%/${indicators})`,
-          }}
-        >
+        <div key={`${t.value}-${i}`} className="indicator-section">
+          <div
+            onClick={() => this.onAddThreshold(t.index + 1)}
+            style={{
+              height: '50%',
+              backgroundColor: t.color,
+            }}
+          />
           <div
           <div
-            onClick={() => this.onAddThreshold(this.insertAtIndex(index - 1))}
+            onClick={() => this.onAddThreshold(t.index)}
             style={{
             style={{
-              height: '100%',
-              background: this.getIndicatorColor(index),
+              height: '50%',
+              backgroundColor: t.color,
             }}
             }}
           />
           />
         </div>
         </div>
       );
       );
-    }
+    });
+  }
 
 
+  renderBaseIndicator() {
     return (
     return (
-      <div
-        key={index}
-        className="indicator-section"
-        style={{
-          height: `calc(100%/${indicators})`,
-        }}
-      >
+      <div className="indicator-section" style={{ height: '100%' }}>
         <div
         <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),
-          }}
+          onClick={() => this.onAddThreshold(0)}
+          style={{ height: '100%', backgroundColor: this.props.options.baseColor }}
         />
         />
       </div>
       </div>
     );
     );
   }
   }
 
 
-  renderIndicator() {
-    const { thresholds } = this.state;
+  renderBase() {
+    const { baseColor } = this.props.options;
 
 
-    return thresholds.map((t, i) => {
-      if (i <= thresholds.length - 1) {
-        return this.renderIndicatorSection(i);
-      }
-
-      return null;
-    });
+    return (
+      <div className="threshold-row threshold-row-base">
+        <div className="threshold-row-inner threshold-row-inner--base">
+          <div className="threshold-row-color">
+            <div className="threshold-row-color-inner">
+              <ColorPicker color={baseColor} onChange={color => this.onChangeBaseColor(color)} />
+            </div>
+          </div>
+          <div className="threshold-row-label">Base</div>
+        </div>
+      </div>
+    );
   }
   }
 
 
   render() {
   render() {
-    const { thresholds } = this.state;
-
     return (
     return (
       <div className="section gf-form-group">
       <div className="section gf-form-group">
-        <h5 className="page-heading">Thresholds</h5>
+        <h5 className="section-heading">Thresholds</h5>
         <div className="thresholds">
         <div className="thresholds">
-          <div className="color-indicators">{this.renderIndicator()}</div>
+          <div className="color-indicators">
+            {this.renderIndicator()}
+            {this.renderBaseIndicator()}
+          </div>
           <div className="threshold-rows">
           <div className="threshold-rows">
-            {thresholds.length > 2 ? this.renderThresholds() : this.renderNoThresholds()}
+            {this.renderThresholds()}
+            {this.renderBase()}
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>

+ 1 - 1
public/app/plugins/panel/gauge/ValueMappings.tsx

@@ -76,7 +76,7 @@ export default class ValueMappings extends PureComponent<OptionModuleProps, Stat
 
 
     return (
     return (
       <div className="section gf-form-group">
       <div className="section gf-form-group">
-        <h5 className="page-heading">Value mappings</h5>
+        <h5 className="section-heading">Value mappings</h5>
         <div>
         <div>
           {mappings.length > 0 &&
           {mappings.length > 0 &&
             mappings.map((mapping, index) => (
             mappings.map((mapping, index) => (

+ 6 - 6
public/app/plugins/panel/gauge/ValueOptions.tsx

@@ -40,8 +40,8 @@ export default class ValueOptions extends PureComponent<OptionModuleProps> {
 
 
     return (
     return (
       <div className="section gf-form-group">
       <div className="section gf-form-group">
-        <h5 className="page-heading">Value</h5>
-        <div className="gf-form-inline">
+        <h5 className="section-heading">Value</h5>
+        <div className="gf-form">
           <Label width={labelWidth}>Stat</Label>
           <Label width={labelWidth}>Stat</Label>
           <Select
           <Select
             width={12}
             width={12}
@@ -50,11 +50,11 @@ export default class ValueOptions extends PureComponent<OptionModuleProps> {
             value={statOptions.find(option => option.value === stat)}
             value={statOptions.find(option => option.value === stat)}
           />
           />
         </div>
         </div>
-        <div className="gf-form-inline">
+        <div className="gf-form">
           <Label width={labelWidth}>Unit</Label>
           <Label width={labelWidth}>Unit</Label>
           <UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
           <UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
         </div>
         </div>
-        <div className="gf-form-inline">
+        <div className="gf-form">
           <Label width={labelWidth}>Decimals</Label>
           <Label width={labelWidth}>Decimals</Label>
           <input
           <input
             className="gf-form-input width-12"
             className="gf-form-input width-12"
@@ -64,11 +64,11 @@ export default class ValueOptions extends PureComponent<OptionModuleProps> {
             onChange={this.onDecimalChange}
             onChange={this.onDecimalChange}
           />
           />
         </div>
         </div>
-        <div className="gf-form-inline">
+        <div className="gf-form">
           <Label width={labelWidth}>Prefix</Label>
           <Label width={labelWidth}>Prefix</Label>
           <input className="gf-form-input width-12" type="text" value={prefix || ''} onChange={this.onPrefixChange} />
           <input className="gf-form-input width-12" type="text" value={prefix || ''} onChange={this.onPrefixChange} />
         </div>
         </div>
-        <div className="gf-form-inline">
+        <div className="gf-form">
           <Label width={labelWidth}>Suffix</Label>
           <Label width={labelWidth}>Suffix</Label>
           <input className="gf-form-input width-12" type="text" value={suffix || ''} onChange={this.onSuffixChange} />
           <input className="gf-form-input width-12" type="text" value={suffix || ''} onChange={this.onSuffixChange} />
         </div>
         </div>

+ 1 - 1
public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap

@@ -5,7 +5,7 @@ exports[`Render should render component 1`] = `
   className="section gf-form-group"
   className="section gf-form-group"
 >
 >
   <h5
   <h5
-    className="page-heading"
+    className="section-heading"
   >
   >
     Value mappings
     Value mappings
   </h5>
   </h5>

+ 7 - 6
public/app/plugins/panel/gauge/module.tsx

@@ -16,15 +16,18 @@ import {
 } from 'app/types';
 } from 'app/types';
 
 
 export interface OptionsProps {
 export interface OptionsProps {
+  baseColor: string;
   decimals: number;
   decimals: number;
+  mappings: Array<RangeMap | ValueMap>;
+  maxValue: number;
+  minValue: number;
   prefix: string;
   prefix: string;
   showThresholdLabels: boolean;
   showThresholdLabels: boolean;
   showThresholdMarkers: boolean;
   showThresholdMarkers: boolean;
   stat: string;
   stat: string;
   suffix: string;
   suffix: string;
-  unit: string;
   thresholds: Threshold[];
   thresholds: Threshold[];
-  mappings: Array<RangeMap | ValueMap>;
+  unit: string;
 }
 }
 
 
 export interface OptionModuleProps {
 export interface OptionModuleProps {
@@ -34,6 +37,7 @@ export interface OptionModuleProps {
 
 
 export const defaultProps = {
 export const defaultProps = {
   options: {
   options: {
+    baseColor: BasicGaugeColor.Green,
     minValue: 0,
     minValue: 0,
     maxValue: 100,
     maxValue: 100,
     prefix: '',
     prefix: '',
@@ -44,10 +48,7 @@ export const defaultProps = {
     stat: '',
     stat: '',
     unit: '',
     unit: '',
     mappings: [],
     mappings: [],
-    thresholds: [
-      { index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
-      { index: 1, label: 'Max', value: 100, canRemove: false },
-    ],
+    thresholds: [],
   },
   },
 };
 };
 
 

+ 2 - 5
public/app/types/panel.ts

@@ -31,10 +31,8 @@ export interface PanelMenuItem {
 
 
 export interface Threshold {
 export interface Threshold {
   index: number;
   index: number;
-  label: string;
   value: number;
   value: number;
   color?: string;
   color?: string;
-  canRemove: boolean;
 }
 }
 
 
 export enum MappingType {
 export enum MappingType {
@@ -43,9 +41,8 @@ export enum MappingType {
 }
 }
 
 
 export enum BasicGaugeColor {
 export enum BasicGaugeColor {
-  Green = 'rgba(50, 172, 45, 0.97)',
-  Orange = 'rgba(237, 129, 40, 0.89)',
-  Red = 'rgb(212, 74, 58)',
+  Green = '#299c46',
+  Red = '#d44a3a',
 }
 }
 
 
 interface BaseMap {
 interface BaseMap {

+ 56 - 27
public/app/viz/Gauge.tsx

@@ -1,10 +1,11 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import $ from 'jquery';
 import $ from 'jquery';
-import { MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } from 'app/types';
+import { BasicGaugeColor, MappingType, RangeMap, Threshold, TimeSeriesVMs, ValueMap } 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 {
+  baseColor: string;
   decimals: number;
   decimals: number;
   height: number;
   height: number;
   mappings: Array<RangeMap | ValueMap>;
   mappings: Array<RangeMap | ValueMap>;
@@ -25,6 +26,7 @@ export class Gauge extends PureComponent<Props> {
   canvasElement: any;
   canvasElement: any;
 
 
   static defaultProps = {
   static defaultProps = {
+    baseColor: BasicGaugeColor.Green,
     maxValue: 100,
     maxValue: 100,
     mappings: [],
     mappings: [],
     minValue: 0,
     minValue: 0,
@@ -32,11 +34,9 @@ export class Gauge extends PureComponent<Props> {
     showThresholdMarkers: true,
     showThresholdMarkers: true,
     showThresholdLabels: false,
     showThresholdLabels: false,
     suffix: '',
     suffix: '',
-    thresholds: [
-      { label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
-      { label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
-    ],
+    thresholds: [],
     unit: 'none',
     unit: 'none',
+    stat: 'avg',
   };
   };
 
 
   componentDidMount() {
   componentDidMount() {
@@ -92,12 +92,44 @@ export class Gauge extends PureComponent<Props> {
     return `${prefix} ${formattedValue} ${suffix}`;
     return `${prefix} ${formattedValue} ${suffix}`;
   }
   }
 
 
+  getFontColor(value) {
+    const { baseColor, maxValue, thresholds } = this.props;
+
+    const atThreshold = thresholds.filter(threshold => value <= threshold.value);
+
+    if (atThreshold.length > 0) {
+      return atThreshold[0].color;
+    } else if (value <= maxValue) {
+      return BasicGaugeColor.Red;
+    }
+
+    return baseColor;
+  }
+
   draw() {
   draw() {
-    const { timeSeries, showThresholdLabels, showThresholdMarkers, thresholds, width, height, stat } = this.props;
+    const {
+      baseColor,
+      maxValue,
+      minValue,
+      timeSeries,
+      showThresholdLabels,
+      showThresholdMarkers,
+      thresholds,
+      width,
+      height,
+      stat,
+    } = this.props;
+
+    let value: string | number = '';
+
+    if (timeSeries[0]) {
+      value = timeSeries[0].stats[stat];
+    } else {
+      value = 'N/A';
+    }
 
 
     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 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 gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
     const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
@@ -105,20 +137,26 @@ export class Gauge extends PureComponent<Props> {
     const thresholdMarkersWidth = gaugeWidth / 5;
     const thresholdMarkersWidth = gaugeWidth / 5;
     const thresholdLabelFontSize = fontSize / 2.5;
     const thresholdLabelFontSize = fontSize / 2.5;
 
 
-    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 formattedThresholds = [
+      { value: minValue, color: BasicGaugeColor.Green },
+      ...thresholds.map((threshold, index) => {
+        return {
+          value: threshold.value,
+          color: index === 0 ? threshold.color : thresholds[index].color,
+        };
+      }),
+      {
+        value: maxValue,
+        color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor,
+      },
+    ];
 
 
     const options = {
     const options = {
       series: {
       series: {
         gauges: {
         gauges: {
           gauge: {
           gauge: {
-            min: thresholds[0].value,
-            max: thresholds[thresholds.length - 1].value,
+            min: minValue,
+            max: maxValue,
             background: { color: backgroundColor },
             background: { color: backgroundColor },
             border: { color: null },
             border: { color: null },
             shadow: { show: false },
             shadow: { show: false },
@@ -139,13 +177,9 @@ export class Gauge extends PureComponent<Props> {
             width: thresholdMarkersWidth,
             width: thresholdMarkersWidth,
           },
           },
           value: {
           value: {
-            color: fontColor,
+            color: this.getFontColor(value),
             formatter: () => {
             formatter: () => {
-              if (timeSeries[0]) {
-                return this.formatValue(timeSeries[0].stats[stat]);
-              }
-
-              return '';
+              return this.formatValue(value);
             },
             },
             font: {
             font: {
               size: fontSize,
               size: fontSize,
@@ -157,11 +191,6 @@ export class Gauge extends PureComponent<Props> {
       },
       },
     };
     };
 
 
-    let value: string | number = 'N/A';
-    if (timeSeries.length) {
-      value = timeSeries[0].stats[stat];
-    }
-
     const plotSeries = {
     const plotSeries = {
       data: [[0, value]],
       data: [[0, value]],
     };
     };

+ 9 - 0
public/sass/components/_gf-form.scss

@@ -123,6 +123,15 @@ $input-border: 1px solid $input-border-color;
     padding-left: 0px;
     padding-left: 0px;
   }
   }
 
 
+  &--btn {
+    border-right: $input-btn-border-width solid $input-label-border-color;
+    border-radius: $border-radius;
+
+    &:hover {
+      background: $list-item-hover-bg;
+    }
+  }
+
   &:disabled {
   &:disabled {
     color: $text-color-weak;
     color: $text-color-weak;
   }
   }

+ 10 - 12
public/sass/components/_thresholds.scss

@@ -1,6 +1,5 @@
 .thresholds {
 .thresholds {
   display: flex;
   display: flex;
-  margin-top: 30px;
 }
 }
 
 
 .threshold-rows {
 .threshold-rows {
@@ -10,7 +9,7 @@
 .threshold-row {
 .threshold-row {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  margin: 5px 0;
+  margin-top: 3px;
   padding: 5px;
   padding: 5px;
 
 
   &::before {
   &::before {
@@ -25,8 +24,11 @@
   border-radius: $border-radius;
   border-radius: $border-radius;
   display: flex;
   display: flex;
   overflow: hidden;
   overflow: hidden;
-  width: 300px;
   height: 37px;
   height: 37px;
+
+  &--base {
+    width: auto;
+  }
 }
 }
 
 
 .threshold-row-color {
 .threshold-row-color {
@@ -48,13 +50,12 @@
 
 
 .threshold-row-input {
 .threshold-row-input {
   padding: 8px 10px;
   padding: 8px 10px;
-  width: 230px;
+  width: 150px;
 }
 }
 
 
 .threshold-row-label {
 .threshold-row-label {
   background-color: $input-label-bg;
   background-color: $input-label-bg;
   padding: 5px;
   padding: 5px;
-  width: 36px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
 }
 }
@@ -65,12 +66,7 @@
   padding: 5px 8px;
   padding: 5px 8px;
 }
 }
 
 
-.threshold-row-min {
-  margin-top: -22px;
-}
-
-.threshold-row-max {
-  margin-bottom: -22px;
+.threshold-row-base {
 }
 }
 
 
 .threshold-row-remove {
 .threshold-row-remove {
@@ -98,11 +94,13 @@
 
 
 .indicator-section {
 .indicator-section {
   width: 100%;
   width: 100%;
+  height: 50px;
   cursor: pointer;
   cursor: pointer;
 }
 }
 
 
 .color-indicators {
 .color-indicators {
   width: 15px;
   width: 15px;
-  border-radius: $border-radius;
+  border-bottom-left-radius: $border-radius;
+  border-bottom-right-radius: $border-radius;
   overflow: hidden;
   overflow: hidden;
 }
 }

+ 0 - 8
public/sass/components/_value-mappings.scss

@@ -3,14 +3,6 @@
   margin-bottom: 10px;
   margin-bottom: 10px;
 }
 }
 
 
-.mapping-row-type {
-  margin-right: 5px;
-}
-
-.mapping-row-input {
-  margin-right: 5px;
-}
-
 .add-mapping-row {
 .add-mapping-row {
   display: flex;
   display: flex;
   overflow: hidden;
   overflow: hidden;