Browse Source

Merge pull request #14461 from grafana/gauge-value-mappings

Gauge value mappings
Torkel Ödegaard 7 years ago
parent
commit
8994f3c283

+ 3 - 3
public/app/core/components/Picker/SimplePicker.tsx

@@ -11,7 +11,7 @@ interface Props {
   onSelected: (item: any) => {} | void;
   onSelected: (item: any) => {} | void;
   options: any[];
   options: any[];
   placeholder?: string;
   placeholder?: string;
-  width: number;
+  width?: number;
   value: any;
   value: any;
 }
 }
 
 
@@ -28,8 +28,8 @@ const SimplePicker: SFC<Props> = ({
 }) => {
 }) => {
   return (
   return (
     <Select
     <Select
-      classNamePrefix={`gf-form-select-box`}
-      className={`width-${width} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
+      classNamePrefix="gf-form-select-box"
+      className={`${width ? 'width-' + width : ''} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
       components={{
       components={{
         Option: DescriptionOption,
         Option: DescriptionOption,
       }}
       }}

+ 1 - 1
public/app/core/components/Picker/Unit/UnitPicker.tsx

@@ -63,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-${width} gf-form-input--form-dropdown`}
+        className={`width-${width} gf-form-input gf-form-input--form-dropdown`}
         defaultValue={value}
         defaultValue={value}
         isSearchable={true}
         isSearchable={true}
         menuShouldScrollIntoView={false}
         menuShouldScrollIntoView={false}

+ 155 - 0
public/app/plugins/panel/gauge/MappingRow.tsx

@@ -0,0 +1,155 @@
+import React, { PureComponent } from 'react';
+import { Label } from 'app/core/components/Label/Label';
+import SimplePicker from 'app/core/components/Picker/SimplePicker';
+import { MappingType, RangeMap, ValueMap } from 'app/types';
+
+interface Props {
+  mapping: ValueMap | RangeMap;
+  updateMapping: (mapping) => void;
+  removeMapping: () => void;
+}
+
+interface State {
+  from: string;
+  id: number;
+  operator: string;
+  text: string;
+  to: string;
+  type: MappingType;
+  value: string;
+}
+
+const mappingOptions = [
+  { value: MappingType.ValueToText, label: 'Value' },
+  { value: MappingType.RangeToText, label: 'Range' },
+];
+
+export default class MappingRow extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      ...props.mapping,
+    };
+  }
+
+  onMappingValueChange = event => {
+    this.setState({ value: event.target.value });
+  };
+
+  onMappingFromChange = event => {
+    this.setState({ from: event.target.value });
+  };
+
+  onMappingToChange = event => {
+    this.setState({ to: event.target.value });
+  };
+
+  onMappingTextChange = event => {
+    this.setState({ text: event.target.value });
+  };
+
+  onMappingTypeChange = mappingType => {
+    this.setState({ type: mappingType });
+  };
+
+  updateMapping = () => {
+    this.props.updateMapping({ ...this.state });
+  };
+
+  renderRow() {
+    const { from, text, to, type, value } = this.state;
+
+    if (type === MappingType.RangeToText) {
+      return (
+        <div className="gf-form">
+          <div className="gf-form-inline mapping-row-input">
+            <Label width={4}>From</Label>
+            <div>
+              <input
+                className="gf-form-input"
+                value={from}
+                onBlur={this.updateMapping}
+                onChange={this.onMappingFromChange}
+              />
+            </div>
+          </div>
+          <div className="gf-form-inline mapping-row-input">
+            <Label width={4}>To</Label>
+            <div>
+              <input
+                className="gf-form-input"
+                value={to}
+                onBlur={this.updateMapping}
+                onChange={this.onMappingToChange}
+              />
+            </div>
+          </div>
+          <div className="gf-form-inline mapping-row-input">
+            <Label width={4}>Text</Label>
+            <div>
+              <input
+                className="gf-form-input"
+                value={text}
+                onBlur={this.updateMapping}
+                onChange={this.onMappingTextChange}
+              />
+            </div>
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <div className="gf-form">
+        <div className="gf-form-inline mapping-row-input">
+          <Label width={4}>Value</Label>
+          <div>
+            <input
+              className="gf-form-input"
+              onBlur={this.updateMapping}
+              onChange={this.onMappingValueChange}
+              value={value}
+            />
+          </div>
+        </div>
+        <div className="gf-form-inline mapping-row-input">
+          <Label width={4}>Text</Label>
+          <div>
+            <input
+              className="gf-form-input"
+              onBlur={this.updateMapping}
+              value={text}
+              onChange={this.onMappingTextChange}
+            />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  render() {
+    const { type } = this.state;
+
+    return (
+      <div className="mapping-row">
+        <div className="gf-form-inline mapping-row-type">
+          <Label width={5}>Type</Label>
+          <SimplePicker
+            placeholder="Choose type"
+            options={mappingOptions}
+            value={mappingOptions.find(o => o.value === type)}
+            getOptionLabel={i => i.label}
+            getOptionValue={i => i.value}
+            onSelected={type => this.onMappingTypeChange(type.value)}
+            width={7}
+          />
+        </div>
+        <div>{this.renderRow()}</div>
+        <div onClick={this.props.removeMapping} className="threshold-row-remove">
+          <i className="fa fa-times" />
+        </div>
+      </div>
+    );
+  }
+}

+ 16 - 9
public/app/plugins/panel/gauge/Threshold.test.tsx

@@ -1,13 +1,19 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 import Thresholds from './Thresholds';
 import Thresholds from './Thresholds';
-import { OptionsProps } from './module';
+import { defaultProps, OptionsProps } from './module';
 import { PanelOptionsProps } from '../../../types';
 import { PanelOptionsProps } from '../../../types';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: PanelOptionsProps<OptionsProps> = {
   const props: PanelOptionsProps<OptionsProps> = {
     onChange: jest.fn(),
     onChange: jest.fn(),
-    options: {} as OptionsProps,
+    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 },
+      ],
+    },
   };
   };
 
 
   Object.assign(props, propOverrides);
   Object.assign(props, propOverrides);
@@ -15,12 +21,6 @@ const setup = (propOverrides?: object) => {
   return shallow(<Thresholds {...props} />).instance() as Thresholds;
   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', () => {
 describe('Add threshold', () => {
   it('should add threshold between min and max', () => {
   it('should add threshold between min and max', () => {
     const instance = setup();
     const instance = setup();
@@ -36,7 +36,14 @@ describe('Add threshold', () => {
 
 
   it('should add threshold between min and added threshold', () => {
   it('should add threshold between min and added threshold', () => {
     const instance = setup({
     const instance = setup({
-      options: { thresholds: thresholds },
+      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 },
+        ],
+      },
     });
     });
 
 
     instance.onAddThreshold(1);
     instance.onAddThreshold(1);

+ 2 - 11
public/app/plugins/panel/gauge/Thresholds.tsx

@@ -2,27 +2,18 @@ import React, { PureComponent } from 'react';
 import classNames from 'classnames/bind';
 import classNames from 'classnames/bind';
 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 { Threshold } from 'app/types';
+import { BasicGaugeColor, Threshold } from 'app/types';
 
 
 interface State {
 interface State {
   thresholds: Threshold[];
   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> {
 export default class Thresholds extends PureComponent<OptionModuleProps, State> {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.state = {
     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 },
-      ],
+      thresholds: props.options.thresholds,
     };
     };
   }
   }
 
 

+ 73 - 0
public/app/plugins/panel/gauge/ValueMappings.test.tsx

@@ -0,0 +1,73 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import ValueMappings from './ValueMappings';
+import { defaultProps, OptionModuleProps } from './module';
+import { MappingType } from 'app/types';
+
+const setup = (propOverrides?: object) => {
+  const props: OptionModuleProps = {
+    onChange: jest.fn(),
+    options: {
+      ...defaultProps.options,
+      mappings: [
+        { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
+        { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
+      ],
+    },
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<ValueMappings {...props} />);
+
+  const instance = wrapper.instance() as ValueMappings;
+
+  return {
+    instance,
+    wrapper,
+  };
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const { wrapper } = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe('On remove mapping', () => {
+  it('Should remove mapping with id 0', () => {
+    const { instance } = setup();
+    instance.onRemoveMapping(1);
+
+    expect(instance.state.mappings).toEqual([
+      { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
+    ]);
+  });
+
+  it('should remove mapping with id 1', () => {
+    const { instance } = setup();
+    instance.onRemoveMapping(2);
+
+    expect(instance.state.mappings).toEqual([
+      { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
+    ]);
+  });
+});
+
+describe('Next id to add', () => {
+  it('should be 4', () => {
+    const { instance } = setup();
+
+    instance.addMapping();
+
+    expect(instance.state.nextIdToAdd).toEqual(4);
+  });
+
+  it('should default to 1', () => {
+    const { instance } = setup({ options: { ...defaultProps.options } });
+
+    expect(instance.state.nextIdToAdd).toEqual(1);
+  });
+});

+ 100 - 0
public/app/plugins/panel/gauge/ValueMappings.tsx

@@ -0,0 +1,100 @@
+import React, { PureComponent } from 'react';
+import MappingRow from './MappingRow';
+import { OptionModuleProps } from './module';
+import { MappingType, RangeMap, ValueMap } from 'app/types';
+
+interface State {
+  mappings: Array<ValueMap | RangeMap>;
+  nextIdToAdd: number;
+}
+
+export default class ValueMappings extends PureComponent<OptionModuleProps, State> {
+  constructor(props) {
+    super(props);
+
+    const mappings = props.options.mappings;
+
+    this.state = {
+      mappings: mappings || [],
+      nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromMappings(mappings) : 1,
+    };
+  }
+
+  getMaxIdFromMappings(mappings) {
+    return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
+  }
+
+  addMapping = () =>
+    this.setState(prevState => ({
+      mappings: [
+        ...prevState.mappings,
+        {
+          id: prevState.nextIdToAdd,
+          operator: '',
+          value: '',
+          text: '',
+          type: MappingType.ValueToText,
+          from: '',
+          to: '',
+        },
+      ],
+      nextIdToAdd: prevState.nextIdToAdd + 1,
+    }));
+
+  onRemoveMapping = id => {
+    this.setState(
+      prevState => ({
+        mappings: prevState.mappings.filter(m => {
+          return m.id !== id;
+        }),
+      }),
+      () => {
+        this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
+      }
+    );
+  };
+
+  updateGauge = mapping => {
+    this.setState(
+      prevState => ({
+        mappings: prevState.mappings.map(m => {
+          if (m.id === mapping.id) {
+            return { ...mapping };
+          }
+
+          return m;
+        }),
+      }),
+      () => {
+        this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
+      }
+    );
+  };
+
+  render() {
+    const { mappings } = this.state;
+
+    return (
+      <div className="section gf-form-group">
+        <h5 className="page-heading">Value mappings</h5>
+        <div>
+          {mappings.length > 0 &&
+            mappings.map((mapping, index) => (
+              <MappingRow
+                key={`${mapping.text}-${index}`}
+                mapping={mapping}
+                updateMapping={this.updateGauge}
+                removeMapping={() => this.onRemoveMapping(mapping.id)}
+              />
+            ))}
+        </div>
+        <div className="add-mapping-row" onClick={this.addMapping}>
+          <div className="add-mapping-row-icon">
+            <i className="fa fa-plus" />
+          </div>
+          <div className="add-mapping-row-label">Add mapping</div>
+        </div>
+      </div>
+    );
+  }
+}

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

@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="section gf-form-group"
+>
+  <h5
+    className="page-heading"
+  >
+    Value mappings
+  </h5>
+  <div>
+    <MappingRow
+      key="Ok-0"
+      mapping={
+        Object {
+          "id": 1,
+          "operator": "",
+          "text": "Ok",
+          "type": 1,
+          "value": "20",
+        }
+      }
+      removeMapping={[Function]}
+      updateMapping={[Function]}
+    />
+    <MappingRow
+      key="Meh-1"
+      mapping={
+        Object {
+          "from": "21",
+          "id": 2,
+          "operator": "",
+          "text": "Meh",
+          "to": "30",
+          "type": 2,
+        }
+      }
+      removeMapping={[Function]}
+      updateMapping={[Function]}
+    />
+  </div>
+  <div
+    className="add-mapping-row"
+    onClick={[Function]}
+  >
+    <div
+      className="add-mapping-row-icon"
+    >
+      <i
+        className="fa fa-plus"
+      />
+    </div>
+    <div
+      className="add-mapping-row-label"
+    >
+      Add mapping
+    </div>
+  </div>
+</div>
+`;

+ 28 - 4
public/app/plugins/panel/gauge/module.tsx

@@ -1,10 +1,19 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import Gauge from 'app/viz/Gauge';
 import Gauge from 'app/viz/Gauge';
-import { NullValueMode, PanelOptionsProps, PanelProps, Threshold } from 'app/types';
 import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
 import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
 import ValueOptions from './ValueOptions';
 import ValueOptions from './ValueOptions';
 import GaugeOptions from './GaugeOptions';
 import GaugeOptions from './GaugeOptions';
 import Thresholds from './Thresholds';
 import Thresholds from './Thresholds';
+import ValueMappings from './ValueMappings';
+import {
+  BasicGaugeColor,
+  NullValueMode,
+  PanelOptionsProps,
+  PanelProps,
+  RangeMap,
+  Threshold,
+  ValueMap,
+} from 'app/types';
 
 
 export interface OptionsProps {
 export interface OptionsProps {
   decimals: number;
   decimals: number;
@@ -15,6 +24,7 @@ export interface OptionsProps {
   suffix: string;
   suffix: string;
   unit: string;
   unit: string;
   thresholds: Threshold[];
   thresholds: Threshold[];
+  mappings: Array<RangeMap | ValueMap>;
 }
 }
 
 
 export interface OptionModuleProps {
 export interface OptionModuleProps {
@@ -30,6 +40,14 @@ export const defaultProps = {
     showThresholdMarkers: true,
     showThresholdMarkers: true,
     showThresholdLabels: false,
     showThresholdLabels: false,
     suffix: '',
     suffix: '',
+    decimals: 0,
+    stat: '',
+    unit: '',
+    mappings: [],
+    thresholds: [
+      { index: 0, label: 'Min', value: 0, canRemove: false, color: BasicGaugeColor.Green },
+      { index: 1, label: 'Max', value: 100, canRemove: false },
+    ],
   },
   },
 };
 };
 
 
@@ -52,11 +70,17 @@ class Options extends PureComponent<PanelOptionsProps<OptionsProps>> {
   static defaultProps = defaultProps;
   static defaultProps = defaultProps;
 
 
   render() {
   render() {
+    const { onChange, options } = this.props;
     return (
     return (
       <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 className="form-section">
+          <ValueOptions onChange={onChange} options={options} />
+          <GaugeOptions onChange={onChange} options={options} />
+          <Thresholds onChange={onChange} options={options} />
+        </div>
+        <div className="form-section">
+          <ValueMappings onChange={onChange} options={options} />
+        </div>
       </div>
       </div>
     );
     );
   }
   }

+ 5 - 1
public/app/types/index.ts

@@ -21,7 +21,7 @@ import {
   DataQueryOptions,
   DataQueryOptions,
   IntervalValues,
   IntervalValues,
 } from './series';
 } from './series';
-import { PanelProps, PanelOptionsProps, Threshold } from './panel';
+import { BasicGaugeColor, MappingType, PanelProps, PanelOptionsProps, RangeMap, Threshold, ValueMap } 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 {
@@ -93,7 +93,11 @@ export {
   Threshold,
   Threshold,
   ValidationEvents,
   ValidationEvents,
   ValidationRule,
   ValidationRule,
+  ValueMap,
+  RangeMap,
   IntervalValues,
   IntervalValues,
+  MappingType,
+  BasicGaugeColor,
 };
 };
 
 
 export interface StoreState {
 export interface StoreState {

+ 27 - 0
public/app/types/panel.ts

@@ -36,3 +36,30 @@ export interface Threshold {
   color?: string;
   color?: string;
   canRemove: boolean;
   canRemove: boolean;
 }
 }
+
+export enum MappingType {
+  ValueToText = 1,
+  RangeToText = 2,
+}
+
+export enum BasicGaugeColor {
+  Green = 'rgba(50, 172, 45, 0.97)',
+  Orange = 'rgba(237, 129, 40, 0.89)',
+  Red = 'rgb(212, 74, 58)',
+}
+
+interface BaseMap {
+  id: number;
+  operator: string;
+  text: string;
+  type: MappingType;
+}
+
+export interface ValueMap extends BaseMap {
+  value: string;
+}
+
+export interface RangeMap extends BaseMap {
+  from: string;
+  to: string;
+}

+ 47 - 10
public/app/viz/Gauge.tsx

@@ -1,38 +1,42 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import $ from 'jquery';
 import $ from 'jquery';
-import { Threshold, TimeSeriesVMs } from 'app/types';
+import { 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 {
   decimals: number;
   decimals: number;
+  height: number;
+  mappings: Array<RangeMap | ValueMap>;
+  maxValue: number;
+  minValue: number;
+  prefix: string;
   timeSeries: TimeSeriesVMs;
   timeSeries: TimeSeriesVMs;
-  showThresholdMarkers: boolean;
   thresholds: Threshold[];
   thresholds: Threshold[];
+  showThresholdMarkers: boolean;
   showThresholdLabels: boolean;
   showThresholdLabels: boolean;
-  unit: string;
-  width: number;
-  height: number;
   stat: string;
   stat: string;
-  prefix: string;
   suffix: string;
   suffix: string;
+  unit: string;
+  width: number;
 }
 }
 
 
 export class Gauge extends PureComponent<Props> {
 export class Gauge extends PureComponent<Props> {
   canvasElement: any;
   canvasElement: any;
 
 
   static defaultProps = {
   static defaultProps = {
-    minValue: 0,
     maxValue: 100,
     maxValue: 100,
+    mappings: [],
+    minValue: 0,
     prefix: '',
     prefix: '',
     showThresholdMarkers: true,
     showThresholdMarkers: true,
     showThresholdLabels: false,
     showThresholdLabels: false,
     suffix: '',
     suffix: '',
-    unit: 'none',
     thresholds: [
     thresholds: [
       { label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
       { label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' },
       { label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
       { label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' },
     ],
     ],
+    unit: 'none',
   };
   };
 
 
   componentDidMount() {
   componentDidMount() {
@@ -43,16 +47,49 @@ export class Gauge extends PureComponent<Props> {
     this.draw();
     this.draw();
   }
   }
 
 
+  formatWithMappings(mappings, value) {
+    const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
+    const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
+
+    const valueMap = valueMaps.map(mapping => {
+      if (mapping.value && value === mapping.value) {
+        return mapping.text;
+      }
+    })[0];
+
+    const rangeMap = rangeMaps.map(mapping => {
+      if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
+        return mapping.text;
+      }
+    })[0];
+
+    return {
+      rangeMap,
+      valueMap,
+    };
+  }
+
   formatValue(value) {
   formatValue(value) {
-    const { decimals, prefix, suffix, unit } = this.props;
+    const { decimals, mappings, prefix, suffix, unit } = this.props;
 
 
     const formatFunc = kbn.valueFormats[unit];
     const formatFunc = kbn.valueFormats[unit];
+    const formattedValue = formatFunc(value, decimals);
+
+    if (mappings.length > 0) {
+      const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue);
+
+      if (valueMap) {
+        return valueMap;
+      } else if (rangeMap) {
+        return rangeMap;
+      }
+    }
 
 
     if (isNaN(value)) {
     if (isNaN(value)) {
       return '-';
       return '-';
     }
     }
 
 
-    return `${prefix} ${formatFunc(value, decimals)} ${suffix}`;
+    return `${prefix} ${formattedValue} ${suffix}`;
   }
   }
 
 
   draw() {
   draw() {

+ 1 - 0
public/sass/_grafana.scss

@@ -106,6 +106,7 @@
 @import 'components/unit-picker';
 @import 'components/unit-picker';
 @import 'components/thresholds';
 @import 'components/thresholds';
 @import 'components/toggle_button_group';
 @import 'components/toggle_button_group';
+@import 'components/value-mappings';
 
 
 // PAGES
 // PAGES
 @import 'pages/login';
 @import 'pages/login';

+ 1 - 1
public/sass/components/_form_select_box.scss

@@ -90,7 +90,7 @@ $select-input-bg-disabled: $input-bg-disabled;
 
 
 .gf-form-select-box__value-container {
 .gf-form-select-box__value-container {
   display: table-cell;
   display: table-cell;
-  padding: 8px 10px;
+  padding: 6px 10px;
   > div {
   > div {
     display: inline-block;
     display: inline-block;
   }
   }

+ 2 - 1
public/sass/components/_thresholds.scss

@@ -77,7 +77,8 @@
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  width: 36px;
+  height: 37px;
+  width: 37px;
   cursor: pointer;
   cursor: pointer;
 }
 }
 
 

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

@@ -0,0 +1,37 @@
+.mapping-row {
+  display: flex;
+  margin-bottom: 10px;
+}
+
+.mapping-row-type {
+  margin-right: 5px;
+}
+
+.mapping-row-input {
+  margin-right: 5px;
+}
+
+.add-mapping-row {
+  display: flex;
+  overflow: hidden;
+  height: 37px;
+  cursor: pointer;
+  border-radius: $border-radius;
+  width: 200px;
+}
+
+.add-mapping-row-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 36px;
+  background-color: $green;
+}
+
+.add-mapping-row-label {
+  align-items: center;
+  display: flex;
+  padding: 5px 8px;
+  background-color: $input-label-bg;
+  width: calc(100% - 36px);
+}