Przeglądaj źródła

FieldDisplay: move threshold and mapping to Field (#17043)

Ryan McKinley 6 lat temu
rodzic
commit
14caa6a068
31 zmienionych plików z 517 dodań i 348 usunięć
  1. 9 0
      packages/grafana-data/src/types/data.ts
  2. 0 1
      packages/grafana-data/src/types/threshold.ts
  3. 17 18
      packages/grafana-data/src/utils/thresholds.ts
  4. 3 3
      packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx
  5. 1 5
      packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
  6. 9 3
      packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
  7. 4 4
      packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
  8. 3 3
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  9. 34 10
      packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts
  10. 59 60
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx
  11. 91 91
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
  12. 1 2
      packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap
  13. 4 4
      packages/grafana-ui/src/utils/displayValue.test.ts
  14. 3 5
      packages/grafana-ui/src/utils/displayValue.ts
  15. 18 11
      packages/grafana-ui/src/utils/fieldDisplay.test.ts
  16. 6 16
      packages/grafana-ui/src/utils/fieldDisplay.ts
  17. 1 16
      public/app/features/dashboard/state/PanelModel.test.ts
  18. 0 11
      public/app/features/dashboard/state/PanelModel.ts
  19. 61 0
      public/app/plugins/panel/bargauge/BarGaugeMigrations.test.ts
  20. 36 0
      public/app/plugins/panel/bargauge/BarGaugeMigrations.ts
  21. 1 2
      public/app/plugins/panel/bargauge/BarGaugePanel.tsx
  22. 14 9
      public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
  23. 36 0
      public/app/plugins/panel/bargauge/__snapshots__/BarGaugeMigrations.test.ts.snap
  24. 3 1
      public/app/plugins/panel/bargauge/module.tsx
  25. 29 12
      public/app/plugins/panel/gauge/GaugeMigrations.ts
  26. 1 2
      public/app/plugins/panel/gauge/GaugePanel.tsx
  27. 14 9
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  28. 29 33
      public/app/plugins/panel/gauge/__snapshots__/GaugeMigrations.test.ts.snap
  29. 8 5
      public/app/plugins/panel/piechart/PieChartPanelEditor.tsx
  30. 14 9
      public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
  31. 8 3
      public/app/plugins/panel/singlestat2/types.ts

+ 9 - 0
packages/grafana-data/src/types/data.ts

@@ -1,3 +1,6 @@
+import { Threshold } from './threshold';
+import { ValueMapping } from './valueMapping';
+
 export enum LoadingState {
 export enum LoadingState {
   NotStarted = 'NotStarted',
   NotStarted = 'NotStarted',
   Loading = 'Loading',
   Loading = 'Loading',
@@ -49,6 +52,12 @@ export interface Field {
   decimals?: number | null; // Significant digits (for display)
   decimals?: number | null; // Significant digits (for display)
   min?: number | null;
   min?: number | null;
   max?: number | null;
   max?: number | null;
+
+  // Convert input values into a display value
+  mappings?: ValueMapping[];
+
+  // Must be sorted by 'value', first value is always -Infinity
+  thresholds?: Threshold[];
 }
 }
 
 
 export interface Labels {
 export interface Labels {

+ 0 - 1
packages/grafana-data/src/types/threshold.ts

@@ -1,5 +1,4 @@
 export interface Threshold {
 export interface Threshold {
-  index: number;
   value: number;
   value: number;
   color: string;
   color: string;
 }
 }

+ 17 - 18
packages/grafana-data/src/utils/thresholds.ts

@@ -1,23 +1,22 @@
 import { Threshold } from '../types';
 import { Threshold } from '../types';
 
 
-export function getThresholdForValue(
-  thresholds: Threshold[],
-  value: number | null | string | undefined
-): Threshold | null {
-  if (thresholds.length === 1) {
-    return thresholds[0];
-  }
-
-  const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
-  if (atThreshold) {
-    return atThreshold;
-  }
-
-  const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
-  if (belowThreshold.length > 0) {
-    const nearestThreshold = belowThreshold.sort((t1: Threshold, t2: Threshold) => t2.value - t1.value)[0];
-    return nearestThreshold;
+export function getActiveThreshold(value: number, thresholds: Threshold[]): Threshold {
+  let active = thresholds[0];
+  for (const threshold of thresholds) {
+    if (value >= threshold.value) {
+      active = threshold;
+    } else {
+      break;
+    }
   }
   }
+  return active;
+}
 
 
-  return null;
+/**
+ * Sorts the thresholds
+ */
+export function sortThresholds(thresholds: Threshold[]) {
+  return thresholds.sort((t1, t2) => {
+    return t1.value - t2.value;
+  });
 }
 }

+ 3 - 3
packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx

@@ -49,9 +49,9 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
       orientation: VizOrientation.Vertical,
       orientation: VizOrientation.Vertical,
       displayMode: 'basic',
       displayMode: 'basic',
       thresholds: [
       thresholds: [
-        { index: 0, value: -Infinity, color: 'green' },
-        { index: 1, value: threshold1Value, color: threshold1Color },
-        { index: 1, value: threshold2Value, color: threshold2Color },
+        { value: -Infinity, color: 'green' },
+        { value: threshold1Value, color: threshold1Color },
+        { value: threshold2Value, color: threshold2Color },
       ],
       ],
     };
     };
 
 

+ 1 - 5
packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx

@@ -25,11 +25,7 @@ function getProps(propOverrides?: Partial<Props>): Props {
     maxValue: 100,
     maxValue: 100,
     minValue: 0,
     minValue: 0,
     displayMode: 'basic',
     displayMode: 'basic',
-    thresholds: [
-      { index: 0, value: -Infinity, color: 'green' },
-      { index: 1, value: 70, color: 'orange' },
-      { index: 2, value: 90, color: 'red' },
-    ],
+    thresholds: [{ value: -Infinity, color: 'green' }, { value: 70, color: 'orange' }, { value: 90, color: 'red' }],
     height: 300,
     height: 300,
     width: 300,
     width: 300,
     value: {
     value: {

+ 9 - 3
packages/grafana-ui/src/components/BarGauge/BarGauge.tsx

@@ -7,7 +7,7 @@ import { getColorFromHexRgbOrName } from '../../utils';
 
 
 // Types
 // Types
 import { DisplayValue, Themeable, VizOrientation } from '../../types';
 import { DisplayValue, Themeable, VizOrientation } from '../../types';
-import { Threshold, TimeSeriesValue, getThresholdForValue } from '@grafana/data';
+import { Threshold, TimeSeriesValue, getActiveThreshold } from '@grafana/data';
 
 
 const MIN_VALUE_HEIGHT = 18;
 const MIN_VALUE_HEIGHT = 18;
 const MAX_VALUE_HEIGHT = 50;
 const MAX_VALUE_HEIGHT = 50;
@@ -87,8 +87,14 @@ export class BarGauge extends PureComponent<Props> {
 
 
   getCellColor(positionValue: TimeSeriesValue): CellColors {
   getCellColor(positionValue: TimeSeriesValue): CellColors {
     const { thresholds, theme, value } = this.props;
     const { thresholds, theme, value } = this.props;
-    const activeThreshold = getThresholdForValue(thresholds, positionValue);
+    if (positionValue === null) {
+      return {
+        background: 'gray',
+        border: 'gray',
+      };
+    }
 
 
+    const activeThreshold = getActiveThreshold(positionValue, thresholds);
     if (activeThreshold !== null) {
     if (activeThreshold !== null) {
       const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
       const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
 
 
@@ -474,7 +480,7 @@ export function getBarGradient(props: Props, maxSize: number): string {
 export function getValueColor(props: Props): string {
 export function getValueColor(props: Props): string {
   const { thresholds, theme, value } = props;
   const { thresholds, theme, value } = props;
 
 
-  const activeThreshold = getThresholdForValue(thresholds, value.numeric);
+  const activeThreshold = getActiveThreshold(value.numeric, thresholds);
 
 
   if (activeThreshold !== null) {
   if (activeThreshold !== null) {
     return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
     return getColorFromHexRgbOrName(activeThreshold.color, theme.type);

+ 4 - 4
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx

@@ -14,7 +14,7 @@ const setup = (propOverrides?: object) => {
     minValue: 0,
     minValue: 0,
     showThresholdMarkers: true,
     showThresholdMarkers: true,
     showThresholdLabels: false,
     showThresholdLabels: false,
-    thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
+    thresholds: [{ value: -Infinity, color: '#7EB26D' }],
     height: 300,
     height: 300,
     width: 300,
     width: 300,
     value: {
     value: {
@@ -48,9 +48,9 @@ describe('Get thresholds formatted', () => {
   it('should get the correct formatted values when thresholds are added', () => {
   it('should get the correct formatted values when thresholds are added', () => {
     const { instance } = setup({
     const { instance } = setup({
       thresholds: [
       thresholds: [
-        { index: 0, value: -Infinity, color: '#7EB26D' },
-        { index: 1, value: 50, color: '#EAB839' },
-        { index: 2, value: 75, color: '#6ED0E0' },
+        { value: -Infinity, color: '#7EB26D' },
+        { value: 50, color: '#EAB839' },
+        { value: 75, color: '#6ED0E0' },
       ],
       ],
     });
     });
 
 

+ 3 - 3
packages/grafana-ui/src/components/Gauge/Gauge.tsx

@@ -43,12 +43,12 @@ export class Gauge extends PureComponent<Props> {
     const lastThreshold = thresholds[thresholds.length - 1];
     const lastThreshold = thresholds[thresholds.length - 1];
 
 
     return [
     return [
-      ...thresholds.map(threshold => {
-        if (threshold.index === 0) {
+      ...thresholds.map((threshold, index) => {
+        if (index === 0) {
           return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
           return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
         }
         }
 
 
-        const previousThreshold = thresholds[threshold.index - 1];
+        const previousThreshold = thresholds[index - 1];
         return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
         return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
       }),
       }),
       { value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
       { value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },

+ 34 - 10
packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts

@@ -3,7 +3,7 @@ import omit from 'lodash/omit';
 
 
 import { VizOrientation, PanelModel } from '../../types/panel';
 import { VizOrientation, PanelModel } from '../../types/panel';
 import { FieldDisplayOptions } from '../../utils/fieldDisplay';
 import { FieldDisplayOptions } from '../../utils/fieldDisplay';
-import { Field, getFieldReducers } from '@grafana/data';
+import { Field, getFieldReducers, Threshold, sortThresholds } from '@grafana/data';
 
 
 export interface SingleStatBaseOptions {
 export interface SingleStatBaseOptions {
   fieldOptions: FieldDisplayOptions;
   fieldOptions: FieldDisplayOptions;
@@ -39,18 +39,16 @@ export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseO
     const { valueOptions } = old;
     const { valueOptions } = old;
 
 
     const fieldOptions = (old.fieldOptions = {} as FieldDisplayOptions);
     const fieldOptions = (old.fieldOptions = {} as FieldDisplayOptions);
-    fieldOptions.mappings = old.valueMappings;
-    fieldOptions.thresholds = old.thresholds;
 
 
     const field = (fieldOptions.defaults = {} as Field);
     const field = (fieldOptions.defaults = {} as Field);
-    if (valueOptions) {
-      field.unit = valueOptions.unit;
-      field.decimals = valueOptions.decimals;
+    field.mappings = old.valueMappings;
+    field.thresholds = migrateOldThresholds(old.thresholds);
+    field.unit = valueOptions.unit;
+    field.decimals = valueOptions.decimals;
 
 
-      // Make sure the stats have a valid name
-      if (valueOptions.stat) {
-        fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
-      }
+    // Make sure the stats have a valid name
+    if (valueOptions.stat) {
+      fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
     }
     }
 
 
     field.min = old.minValue;
     field.min = old.minValue;
@@ -58,7 +56,33 @@ export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseO
 
 
     // remove old props
     // remove old props
     return omit(old, 'valueMappings', 'thresholds', 'valueOptions', 'minValue', 'maxValue');
     return omit(old, 'valueMappings', 'thresholds', 'valueOptions', 'minValue', 'maxValue');
+  } else if (old.fieldOptions) {
+    // Move mappins & thresholds to field defautls (6.4+)
+    const { mappings, thresholds, ...fieldOptions } = old.fieldOptions;
+    fieldOptions.defaults = {
+      mappings,
+      thresholds: migrateOldThresholds(thresholds),
+      ...fieldOptions.defaults,
+    };
+    old.fieldOptions = fieldOptions;
+    return old;
   }
   }
 
 
   return panel.options;
   return panel.options;
 };
 };
+
+export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefined {
+  if (!thresholds || !thresholds.length) {
+    return undefined;
+  }
+  const copy = thresholds.map(t => {
+    return {
+      // Drops 'index'
+      value: t.value === null ? -Infinity : t.value,
+      color: t.color,
+    };
+  });
+  sortThresholds(copy);
+  copy[0].value = -Infinity;
+  return copy;
+}

+ 59 - 60
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx

@@ -1,6 +1,6 @@
 import React, { ChangeEvent } from 'react';
 import React, { ChangeEvent } from 'react';
 import { mount } from 'enzyme';
 import { mount } from 'enzyme';
-import { ThresholdsEditor, Props } from './ThresholdsEditor';
+import { ThresholdsEditor, Props, threshodsWithoutKey } from './ThresholdsEditor';
 import { colors } from '../../utils';
 import { colors } from '../../utils';
 
 
 const setup = (propOverrides?: Partial<Props>) => {
 const setup = (propOverrides?: Partial<Props>) => {
@@ -20,6 +20,10 @@ const setup = (propOverrides?: Partial<Props>) => {
   };
   };
 };
 };
 
 
+function getCurrentThresholds(editor: ThresholdsEditor) {
+  return threshodsWithoutKey(editor.state.thresholds);
+}
+
 describe('Render', () => {
 describe('Render', () => {
   it('should render with base threshold', () => {
   it('should render with base threshold', () => {
     const { wrapper } = setup();
     const { wrapper } = setup();
@@ -32,60 +36,55 @@ describe('Initialization', () => {
   it('should add a base threshold if missing', () => {
   it('should add a base threshold if missing', () => {
     const { instance } = setup();
     const { instance } = setup();
 
 
-    expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]);
+    expect(getCurrentThresholds(instance)).toEqual([{ value: -Infinity, color: colors[0] }]);
   });
   });
 });
 });
 
 
 describe('Add threshold', () => {
 describe('Add threshold', () => {
-  it('should not add threshold at index 0', () => {
-    const { instance } = setup();
-
-    instance.onAddThreshold(0);
-
-    expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]);
-  });
-
   it('should add threshold', () => {
   it('should add threshold', () => {
     const { instance } = setup();
     const { instance } = setup();
 
 
-    instance.onAddThreshold(1);
+    instance.onAddThresholdAfter(instance.state.thresholds[0]);
 
 
-    expect(instance.state.thresholds).toEqual([
-      { index: 0, value: -Infinity, color: colors[0] },
-      { index: 1, value: 50, color: colors[2] },
+    expect(getCurrentThresholds(instance)).toEqual([
+      { value: -Infinity, color: colors[0] }, // 0
+      { value: 50, color: colors[2] }, // 1
     ]);
     ]);
   });
   });
 
 
   it('should add another threshold above a first', () => {
   it('should add another threshold above a first', () => {
     const { instance } = setup({
     const { instance } = setup({
-      thresholds: [{ index: 0, value: -Infinity, color: colors[0] }, { index: 1, value: 50, color: colors[2] }],
+      thresholds: [
+        { value: -Infinity, color: colors[0] }, // 0
+        { value: 50, color: colors[2] }, // 1
+      ],
     });
     });
 
 
-    instance.onAddThreshold(2);
+    instance.onAddThresholdAfter(instance.state.thresholds[1]);
 
 
-    expect(instance.state.thresholds).toEqual([
-      { index: 0, value: -Infinity, color: colors[0] },
-      { index: 1, value: 50, color: colors[2] },
-      { index: 2, value: 75, color: colors[3] },
+    expect(getCurrentThresholds(instance)).toEqual([
+      { value: -Infinity, color: colors[0] }, // 0
+      { value: 50, color: colors[2] }, // 1
+      { value: 75, color: colors[3] }, // 2
     ]);
     ]);
   });
   });
 
 
   it('should add another threshold between first and second index', () => {
   it('should add another threshold between first and second index', () => {
     const { instance } = setup({
     const { instance } = setup({
       thresholds: [
       thresholds: [
-        { index: 0, value: -Infinity, color: colors[0] },
-        { index: 1, value: 50, color: colors[2] },
-        { index: 2, value: 75, color: colors[3] },
+        { value: -Infinity, color: colors[0] },
+        { value: 50, color: colors[2] },
+        { value: 75, color: colors[3] },
       ],
       ],
     });
     });
 
 
-    instance.onAddThreshold(2);
+    instance.onAddThresholdAfter(instance.state.thresholds[1]);
 
 
-    expect(instance.state.thresholds).toEqual([
-      { index: 0, value: -Infinity, color: colors[0] },
-      { index: 1, value: 50, color: colors[2] },
-      { index: 2, value: 62.5, color: colors[4] },
-      { index: 3, value: 75, color: colors[3] },
+    expect(getCurrentThresholds(instance)).toEqual([
+      { value: -Infinity, color: colors[0] },
+      { value: 50, color: colors[2] },
+      { value: 62.5, color: colors[4] },
+      { value: 75, color: colors[3] },
     ]);
     ]);
   });
   });
 });
 });
@@ -93,30 +92,30 @@ describe('Add threshold', () => {
 describe('Remove threshold', () => {
 describe('Remove threshold', () => {
   it('should not remove threshold at index 0', () => {
   it('should not remove threshold at index 0', () => {
     const thresholds = [
     const thresholds = [
-      { index: 0, value: -Infinity, color: '#7EB26D' },
-      { index: 1, value: 50, color: '#EAB839' },
-      { index: 2, value: 75, color: '#6ED0E0' },
+      { value: -Infinity, color: '#7EB26D' },
+      { value: 50, color: '#EAB839' },
+      { value: 75, color: '#6ED0E0' },
     ];
     ];
     const { instance } = setup({ thresholds });
     const { instance } = setup({ thresholds });
 
 
-    instance.onRemoveThreshold(thresholds[0]);
+    instance.onRemoveThreshold(instance.state.thresholds[0]);
 
 
-    expect(instance.state.thresholds).toEqual(thresholds);
+    expect(getCurrentThresholds(instance)).toEqual(thresholds);
   });
   });
 
 
   it('should remove threshold', () => {
   it('should remove threshold', () => {
     const thresholds = [
     const thresholds = [
-      { index: 0, value: -Infinity, color: '#7EB26D' },
-      { index: 1, value: 50, color: '#EAB839' },
-      { index: 2, value: 75, color: '#6ED0E0' },
+      { value: -Infinity, color: '#7EB26D' },
+      { value: 50, color: '#EAB839' },
+      { value: 75, color: '#6ED0E0' },
     ];
     ];
     const { instance } = setup({ thresholds });
     const { instance } = setup({ thresholds });
 
 
-    instance.onRemoveThreshold(thresholds[1]);
+    instance.onRemoveThreshold(instance.state.thresholds[1]);
 
 
-    expect(instance.state.thresholds).toEqual([
-      { index: 0, value: -Infinity, color: '#7EB26D' },
-      { index: 1, value: 75, color: '#6ED0E0' },
+    expect(getCurrentThresholds(instance)).toEqual([
+      { value: -Infinity, color: '#7EB26D' },
+      { value: 75, color: '#6ED0E0' },
     ]);
     ]);
   });
   });
 });
 });
@@ -124,25 +123,25 @@ describe('Remove threshold', () => {
 describe('change threshold value', () => {
 describe('change threshold value', () => {
   it('should not change threshold at index 0', () => {
   it('should not change threshold at index 0', () => {
     const thresholds = [
     const thresholds = [
-      { index: 0, value: -Infinity, color: '#7EB26D' },
-      { index: 1, value: 50, color: '#EAB839' },
-      { index: 2, value: 75, color: '#6ED0E0' },
+      { value: -Infinity, color: '#7EB26D' },
+      { value: 50, color: '#EAB839' },
+      { value: 75, color: '#6ED0E0' },
     ];
     ];
     const { instance } = setup({ thresholds });
     const { instance } = setup({ thresholds });
 
 
     const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
     const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
 
 
-    instance.onChangeThresholdValue(mockEvent, thresholds[0]);
+    instance.onChangeThresholdValue(mockEvent, instance.state.thresholds[0]);
 
 
-    expect(instance.state.thresholds).toEqual(thresholds);
+    expect(getCurrentThresholds(instance)).toEqual(thresholds);
   });
   });
 
 
   it('should update value', () => {
   it('should update value', () => {
     const { instance } = setup();
     const { instance } = setup();
     const thresholds = [
     const thresholds = [
-      { index: 0, value: -Infinity, color: '#7EB26D' },
-      { index: 1, value: 50, color: '#EAB839' },
-      { index: 2, value: 75, color: '#6ED0E0' },
+      { value: -Infinity, color: '#7EB26D', key: 1 },
+      { value: 50, color: '#EAB839', key: 2 },
+      { value: 75, color: '#6ED0E0', key: 3 },
     ];
     ];
 
 
     instance.state = {
     instance.state = {
@@ -153,10 +152,10 @@ describe('change threshold value', () => {
 
 
     instance.onChangeThresholdValue(mockEvent, thresholds[1]);
     instance.onChangeThresholdValue(mockEvent, thresholds[1]);
 
 
-    expect(instance.state.thresholds).toEqual([
-      { index: 0, value: -Infinity, color: '#7EB26D' },
-      { index: 1, value: 78, color: '#EAB839' },
-      { index: 2, value: 75, color: '#6ED0E0' },
+    expect(getCurrentThresholds(instance)).toEqual([
+      { value: -Infinity, color: '#7EB26D' },
+      { value: 78, color: '#EAB839' },
+      { value: 75, color: '#6ED0E0' },
     ]);
     ]);
   });
   });
 });
 });
@@ -165,9 +164,9 @@ describe('on blur threshold value', () => {
   it('should resort rows and update indexes', () => {
   it('should resort rows and update indexes', () => {
     const { instance } = setup();
     const { instance } = setup();
     const thresholds = [
     const thresholds = [
-      { index: 0, value: -Infinity, color: '#7EB26D' },
-      { index: 1, value: 78, color: '#EAB839' },
-      { index: 2, value: 75, color: '#6ED0E0' },
+      { value: -Infinity, color: '#7EB26D', key: 1 },
+      { value: 78, color: '#EAB839', key: 2 },
+      { value: 75, color: '#6ED0E0', key: 3 },
     ];
     ];
 
 
     instance.setState({
     instance.setState({
@@ -176,10 +175,10 @@ describe('on blur threshold value', () => {
 
 
     instance.onBlur();
     instance.onBlur();
 
 
-    expect(instance.state.thresholds).toEqual([
-      { index: 0, value: -Infinity, color: '#7EB26D' },
-      { index: 1, value: 75, color: '#6ED0E0' },
-      { index: 2, value: 78, color: '#EAB839' },
+    expect(getCurrentThresholds(instance)).toEqual([
+      { value: -Infinity, color: '#7EB26D' },
+      { value: 75, color: '#6ED0E0' },
+      { value: 78, color: '#EAB839' },
     ]);
     ]);
   });
   });
 });
 });

+ 91 - 91
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx

@@ -1,5 +1,5 @@
 import React, { PureComponent, ChangeEvent } from 'react';
 import React, { PureComponent, ChangeEvent } from 'react';
-import { Threshold } from '@grafana/data';
+import { Threshold, sortThresholds } from '@grafana/data';
 import { colors } from '../../utils';
 import { colors } from '../../utils';
 import { ThemeContext } from '../../themes';
 import { ThemeContext } from '../../themes';
 import { getColorFromHexRgbOrName } from '../../utils';
 import { getColorFromHexRgbOrName } from '../../utils';
@@ -13,115 +13,121 @@ export interface Props {
 }
 }
 
 
 interface State {
 interface State {
-  thresholds: Threshold[];
+  thresholds: ThresholdWithKey[];
+}
+
+interface ThresholdWithKey extends Threshold {
+  key: number;
 }
 }
 
 
+let counter = 100;
+
 export class ThresholdsEditor extends PureComponent<Props, State> {
 export class ThresholdsEditor extends PureComponent<Props, State> {
   constructor(props: Props) {
   constructor(props: Props) {
     super(props);
     super(props);
 
 
-    const addDefaultThreshold = this.props.thresholds.length === 0;
-    const thresholds: Threshold[] = addDefaultThreshold
-      ? [{ index: 0, value: -Infinity, color: colors[0] }]
-      : props.thresholds;
+    const thresholds = props.thresholds
+      ? props.thresholds.map(t => {
+          return {
+            color: t.color,
+            value: t.value === null ? -Infinity : t.value,
+            key: counter++,
+          };
+        })
+      : ([] as ThresholdWithKey[]);
+
+    let needsCallback = false;
+    if (!thresholds.length) {
+      thresholds.push({ value: -Infinity, color: colors[0], key: counter++ });
+      needsCallback = true;
+    } else {
+      // First value is always base
+      thresholds[0].value = -Infinity;
+    }
+
+    // Update the state
     this.state = { thresholds };
     this.state = { thresholds };
 
 
-    if (addDefaultThreshold) {
+    if (needsCallback) {
       this.onChange();
       this.onChange();
     }
     }
   }
   }
 
 
-  onAddThreshold = (index: number) => {
+  onAddThresholdAfter = (threshold: ThresholdWithKey) => {
     const { thresholds } = this.state;
     const { thresholds } = this.state;
+
     const maxValue = 100;
     const maxValue = 100;
     const minValue = 0;
     const minValue = 0;
 
 
-    if (index === 0) {
-      return;
-    }
-
-    const newThresholds = thresholds.map(threshold => {
-      if (threshold.index >= index) {
-        const index = threshold.index + 1;
-        threshold = { ...threshold, index };
+    let prev: ThresholdWithKey | undefined = undefined;
+    let next: ThresholdWithKey | undefined = undefined;
+    for (const t of thresholds) {
+      if (prev && prev.key === threshold.key) {
+        next = t;
+        break;
       }
       }
-      return threshold;
-    });
+      prev = t;
+    }
 
 
-    // Setting value to a value between the previous thresholds
-    const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0];
-    const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0];
-    const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue;
-    const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue;
-    const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
+    const prevValue = prev && isFinite(prev.value) ? prev.value : minValue;
+    const nextValue = next && isFinite(next.value) ? next.value : maxValue;
 
 
-    // Set a color
-    const color = colors.filter(c => !newThresholds.some(t => t.color === c))[1];
+    const color = colors.filter(c => !thresholds.some(t => t.color === c))[1];
+    const add = {
+      value: prevValue + (nextValue - prevValue) / 2.0,
+      color: color,
+      key: counter++,
+    };
+    const newThresholds = [...thresholds, add];
+    sortThresholds(newThresholds);
 
 
     this.setState(
     this.setState(
       {
       {
-        thresholds: this.sortThresholds([
-          ...newThresholds,
-          {
-            color,
-            index,
-            value: value as number,
-          },
-        ]),
+        thresholds: newThresholds,
       },
       },
       () => this.onChange()
       () => this.onChange()
     );
     );
   };
   };
 
 
-  onRemoveThreshold = (threshold: Threshold) => {
-    if (threshold.index === 0) {
+  onRemoveThreshold = (threshold: ThresholdWithKey) => {
+    const { thresholds } = this.state;
+    if (!thresholds.length) {
+      return;
+    }
+    // Don't remove index 0
+    if (threshold.key === thresholds[0].key) {
       return;
       return;
     }
     }
-
     this.setState(
     this.setState(
-      prevState => {
-        const newThresholds = prevState.thresholds.map(t => {
-          if (t.index > threshold.index) {
-            const index = t.index - 1;
-            t = { ...t, index };
-          }
-          return t;
-        });
-
-        return {
-          thresholds: newThresholds.filter(t => t !== threshold),
-        };
+      {
+        thresholds: thresholds.filter(t => t.key !== threshold.key),
       },
       },
       () => this.onChange()
       () => this.onChange()
     );
     );
   };
   };
 
 
-  onChangeThresholdValue = (event: ChangeEvent<HTMLInputElement>, threshold: Threshold) => {
-    if (threshold.index === 0) {
-      return;
-    }
-
-    const { thresholds } = this.state;
+  onChangeThresholdValue = (event: ChangeEvent<HTMLInputElement>, threshold: ThresholdWithKey) => {
     const cleanValue = event.target.value.replace(/,/g, '.');
     const cleanValue = event.target.value.replace(/,/g, '.');
     const parsedValue = parseFloat(cleanValue);
     const parsedValue = parseFloat(cleanValue);
     const value = isNaN(parsedValue) ? '' : parsedValue;
     const value = isNaN(parsedValue) ? '' : parsedValue;
 
 
-    const newThresholds = thresholds.map(t => {
-      if (t === threshold && t.index !== 0) {
+    const thresholds = this.state.thresholds.map(t => {
+      if (t.key === threshold.key) {
         t = { ...t, value: value as number };
         t = { ...t, value: value as number };
       }
       }
-
       return t;
       return t;
     });
     });
-
-    this.setState({ thresholds: newThresholds });
+    if (thresholds.length) {
+      thresholds[0].value = -Infinity;
+    }
+    this.setState({ thresholds });
   };
   };
 
 
-  onChangeThresholdColor = (threshold: Threshold, color: string) => {
+  onChangeThresholdColor = (threshold: ThresholdWithKey, color: string) => {
     const { thresholds } = this.state;
     const { thresholds } = this.state;
 
 
     const newThresholds = thresholds.map(t => {
     const newThresholds = thresholds.map(t => {
-      if (t === threshold) {
+      if (t.key === threshold.key) {
         t = { ...t, color: color };
         t = { ...t, color: color };
       }
       }
 
 
@@ -137,30 +143,22 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
   };
   };
 
 
   onBlur = () => {
   onBlur = () => {
-    this.setState(prevState => {
-      const sortThresholds = this.sortThresholds([...prevState.thresholds]);
-      let index = 0;
-      sortThresholds.forEach(t => {
-        t.index = index++;
-      });
-
-      return { thresholds: sortThresholds };
-    });
-
-    this.onChange();
+    const thresholds = [...this.state.thresholds];
+    sortThresholds(thresholds);
+    this.setState(
+      {
+        thresholds,
+      },
+      () => this.onChange()
+    );
   };
   };
 
 
   onChange = () => {
   onChange = () => {
-    this.props.onChange(this.state.thresholds);
-  };
-
-  sortThresholds = (thresholds: Threshold[]) => {
-    return thresholds.sort((t1, t2) => {
-      return t1.value - t2.value;
-    });
+    const { thresholds } = this.state;
+    this.props.onChange(threshodsWithoutKey(thresholds));
   };
   };
 
 
-  renderInput = (threshold: Threshold) => {
+  renderInput = (threshold: ThresholdWithKey) => {
     return (
     return (
       <div className="thresholds-row-input-inner">
       <div className="thresholds-row-input-inner">
         <span className="thresholds-row-input-inner-arrow" />
         <span className="thresholds-row-input-inner-arrow" />
@@ -175,12 +173,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
             </div>
             </div>
           )}
           )}
         </div>
         </div>
-        {threshold.index === 0 && (
+        {!isFinite(threshold.value) ? (
           <div className="thresholds-row-input-inner-value">
           <div className="thresholds-row-input-inner-value">
             <Input type="text" value="Base" readOnly />
             <Input type="text" value="Base" readOnly />
           </div>
           </div>
-        )}
-        {threshold.index > 0 && (
+        ) : (
           <>
           <>
             <div className="thresholds-row-input-inner-value">
             <div className="thresholds-row-input-inner-value">
               <Input
               <Input
@@ -189,7 +186,6 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
                 onChange={(event: ChangeEvent<HTMLInputElement>) => this.onChangeThresholdValue(event, threshold)}
                 onChange={(event: ChangeEvent<HTMLInputElement>) => this.onChangeThresholdValue(event, threshold)}
                 value={threshold.value}
                 value={threshold.value}
                 onBlur={this.onBlur}
                 onBlur={this.onBlur}
-                readOnly={threshold.index === 0}
               />
               />
             </div>
             </div>
             <div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
             <div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
@@ -212,13 +208,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
                 {thresholds
                 {thresholds
                   .slice(0)
                   .slice(0)
                   .reverse()
                   .reverse()
-                  .map((threshold, index) => {
+                  .map(threshold => {
                     return (
                     return (
-                      <div className="thresholds-row" key={`${threshold.index}-${index}`}>
-                        <div
-                          className="thresholds-row-add-button"
-                          onClick={() => this.onAddThreshold(threshold.index + 1)}
-                        >
+                      <div className="thresholds-row" key={`${threshold.key}`}>
+                        <div className="thresholds-row-add-button" onClick={() => this.onAddThresholdAfter(threshold)}>
                           <i className="fa fa-plus" />
                           <i className="fa fa-plus" />
                         </div>
                         </div>
                         <div
                         <div
@@ -237,3 +230,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
     );
     );
   }
   }
 }
 }
+
+export function threshodsWithoutKey(thresholds: ThresholdWithKey[]): Threshold[] {
+  return thresholds.map(t => {
+    const { key, ...rest } = t;
+    return rest; // everything except key
+  });
+}

+ 1 - 2
packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap

@@ -9,7 +9,6 @@ exports[`Render should render with base threshold 1`] = `
           Array [
           Array [
             Object {
             Object {
               "color": "#7EB26D",
               "color": "#7EB26D",
-              "index": 0,
               "value": -Infinity,
               "value": -Infinity,
             },
             },
           ],
           ],
@@ -48,7 +47,7 @@ exports[`Render should render with base threshold 1`] = `
         >
         >
           <div
           <div
             className="thresholds-row"
             className="thresholds-row"
-            key="0-0"
+            key="100"
           >
           >
             <div
             <div
               className="thresholds-row-add-button"
               className="thresholds-row-add-button"

+ 4 - 4
packages/grafana-ui/src/utils/displayValue.test.ts

@@ -103,7 +103,7 @@ describe('Format value', () => {
   it('should return if value isNaN', () => {
   it('should return if value isNaN', () => {
     const valueMappings: ValueMapping[] = [];
     const valueMappings: ValueMapping[] = [];
     const value = 'N/A';
     const value = 'N/A';
-    const instance = getDisplayProcessor({ mappings: valueMappings });
+    const instance = getDisplayProcessor({ field: { mappings: valueMappings } });
 
 
     const result = instance(value);
     const result = instance(value);
 
 
@@ -114,7 +114,7 @@ describe('Format value', () => {
     const valueMappings: ValueMapping[] = [];
     const valueMappings: ValueMapping[] = [];
     const value = '6';
     const value = '6';
 
 
-    const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
+    const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
 
 
     const result = instance(value);
     const result = instance(value);
 
 
@@ -127,7 +127,7 @@ describe('Format value', () => {
       { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
       { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
     ];
     ];
     const value = '10';
     const value = '10';
-    const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
+    const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
 
 
     const result = instance(value);
     const result = instance(value);
 
 
@@ -160,7 +160,7 @@ describe('Format value', () => {
       { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
       { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
     ];
     ];
     const value = '11';
     const value = '11';
-    const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
+    const instance = getDisplayProcessor({ field: { decimals: 1, mappings: valueMappings } });
 
 
     expect(instance(value).text).toEqual('1-20');
     expect(instance(value).text).toEqual('1-20');
   });
   });

+ 3 - 5
packages/grafana-ui/src/utils/displayValue.ts

@@ -7,16 +7,13 @@ import { getColorFromHexRgbOrName } from './namedColorsPalette';
 
 
 // Types
 // Types
 import { DecimalInfo, DisplayValue, GrafanaTheme, GrafanaThemeType, DecimalCount } from '../types';
 import { DecimalInfo, DisplayValue, GrafanaTheme, GrafanaThemeType, DecimalCount } from '../types';
-import { DateTime, dateTime, Threshold, ValueMapping, getMappedValue, Field } from '@grafana/data';
+import { DateTime, dateTime, Threshold, getMappedValue, Field } from '@grafana/data';
 
 
 export type DisplayProcessor = (value: any) => DisplayValue;
 export type DisplayProcessor = (value: any) => DisplayValue;
 
 
 export interface DisplayValueOptions {
 export interface DisplayValueOptions {
   field?: Partial<Field>;
   field?: Partial<Field>;
 
 
-  mappings?: ValueMapping[];
-  thresholds?: Threshold[];
-
   // Alternative to empty string
   // Alternative to empty string
   noValue?: string;
   noValue?: string;
 
 
@@ -31,7 +28,8 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce
     const formatFunc = getValueFormat(field.unit || 'none');
     const formatFunc = getValueFormat(field.unit || 'none');
 
 
     return (value: any) => {
     return (value: any) => {
-      const { mappings, thresholds, theme } = options;
+      const { theme } = options;
+      const { mappings, thresholds } = field;
       let color;
       let color;
 
 
       let text = _.toString(value);
       let text = _.toString(value);

+ 18 - 11
packages/grafana-ui/src/utils/fieldDisplay.test.ts

@@ -1,5 +1,5 @@
 import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
 import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
-import { FieldType, ReducerID } from '@grafana/data';
+import { FieldType, ReducerID, Threshold } from '@grafana/data';
 import { GrafanaThemeType } from '../types/theme';
 import { GrafanaThemeType } from '../types/theme';
 import { getTheme } from '../themes/index';
 import { getTheme } from '../themes/index';
 
 
@@ -55,8 +55,6 @@ describe('FieldDisplay', () => {
     },
     },
     fieldOptions: {
     fieldOptions: {
       calcs: [],
       calcs: [],
-      mappings: [],
-      thresholds: [],
       override: {},
       override: {},
       defaults: {},
       defaults: {},
     },
     },
@@ -68,8 +66,6 @@ describe('FieldDisplay', () => {
       ...options,
       ...options,
       fieldOptions: {
       fieldOptions: {
         calcs: [ReducerID.first],
         calcs: [ReducerID.first],
-        mappings: [],
-        thresholds: [],
         override: {},
         override: {},
         defaults: {
         defaults: {
           title: '$__cell_0 * $__field_name * $__series_name',
           title: '$__cell_0 * $__field_name * $__series_name',
@@ -88,8 +84,6 @@ describe('FieldDisplay', () => {
       ...options,
       ...options,
       fieldOptions: {
       fieldOptions: {
         calcs: [ReducerID.last],
         calcs: [ReducerID.last],
-        mappings: [],
-        thresholds: [],
         override: {},
         override: {},
         defaults: {},
         defaults: {},
       },
       },
@@ -104,8 +98,6 @@ describe('FieldDisplay', () => {
         values: true, //
         values: true, //
         limit: 1000,
         limit: 1000,
         calcs: [],
         calcs: [],
-        mappings: [],
-        thresholds: [],
         override: {},
         override: {},
         defaults: {},
         defaults: {},
       },
       },
@@ -120,12 +112,27 @@ describe('FieldDisplay', () => {
         values: true, //
         values: true, //
         limit: 2,
         limit: 2,
         calcs: [],
         calcs: [],
-        mappings: [],
-        thresholds: [],
         override: {},
         override: {},
         defaults: {},
         defaults: {},
       },
       },
     });
     });
     expect(display.map(v => v.display.numeric)).toEqual([1, 3]); // First 2 are from the first field
     expect(display.map(v => v.display.numeric)).toEqual([1, 3]); // First 2 are from the first field
   });
   });
+
+  it('should restore -Infinity value for base threshold', () => {
+    const field = getFieldProperties({
+      thresholds: [
+        ({
+          color: '#73BF69',
+          value: null,
+        } as unknown) as Threshold,
+        {
+          color: '#F2495C',
+          value: 50,
+        },
+      ],
+    });
+    expect(field.thresholds!.length).toEqual(2);
+    expect(field.thresholds![0].value).toBe(-Infinity);
+  });
 });
 });

+ 6 - 16
packages/grafana-ui/src/utils/fieldDisplay.ts

@@ -4,16 +4,7 @@ import toString from 'lodash/toString';
 import { DisplayValue, GrafanaTheme, InterpolateFunction, ScopedVars, GraphSeriesValue } from '../types/index';
 import { DisplayValue, GrafanaTheme, InterpolateFunction, ScopedVars, GraphSeriesValue } from '../types/index';
 import { getDisplayProcessor } from './displayValue';
 import { getDisplayProcessor } from './displayValue';
 import { getFlotPairs } from './flotPairs';
 import { getFlotPairs } from './flotPairs';
-import {
-  ValueMapping,
-  Threshold,
-  ReducerID,
-  reduceField,
-  FieldType,
-  NullValueMode,
-  DataFrame,
-  Field,
-} from '@grafana/data';
+import { ReducerID, reduceField, FieldType, NullValueMode, DataFrame, Field } from '@grafana/data';
 
 
 export interface FieldDisplayOptions {
 export interface FieldDisplayOptions {
   values?: boolean; // If true show each row value
   values?: boolean; // If true show each row value
@@ -22,10 +13,6 @@ export interface FieldDisplayOptions {
 
 
   defaults: Partial<Field>; // Use these values unless otherwise stated
   defaults: Partial<Field>; // Use these values unless otherwise stated
   override: Partial<Field>; // Set these values regardless of the source
   override: Partial<Field>; // Set these values regardless of the source
-
-  // Could these be data driven also?
-  thresholds: Threshold[];
-  mappings: ValueMapping[];
 }
 }
 
 
 export const VAR_SERIES_NAME = '__series_name';
 export const VAR_SERIES_NAME = '__series_name';
@@ -127,8 +114,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
 
 
         const display = getDisplayProcessor({
         const display = getDisplayProcessor({
           field,
           field,
-          mappings: fieldOptions.mappings,
-          thresholds: fieldOptions.thresholds,
           theme: options.theme,
           theme: options.theme,
         });
         });
 
 
@@ -263,6 +248,11 @@ export function getFieldProperties(...props: PartialField[]): Field {
     field = applyFieldProperties(field, props[i]);
     field = applyFieldProperties(field, props[i]);
   }
   }
 
 
+  // First value is always -Infinity
+  if (field.thresholds && field.thresholds.length) {
+    field.thresholds[0].value = -Infinity;
+  }
+
   // Verify that max > min
   // Verify that max > min
   if (field.hasOwnProperty('min') && field.hasOwnProperty('max') && field.min! > field.max!) {
   if (field.hasOwnProperty('min') && field.hasOwnProperty('max') && field.min! > field.max!) {
     return {
     return {

+ 1 - 16
public/app/features/dashboard/state/PanelModel.test.ts

@@ -98,21 +98,6 @@ describe('PanelModel', () => {
       expect(saveModel.events).toBe(undefined);
       expect(saveModel.events).toBe(undefined);
     });
     });
 
 
-    it('should restore -Infinity value for base threshold', () => {
-      expect(model.options.fieldOptions.thresholds).toEqual([
-        {
-          color: '#F2495C',
-          index: 1,
-          value: 50,
-        },
-        {
-          color: '#73BF69',
-          index: 0,
-          value: -Infinity,
-        },
-      ]);
-    });
-
     describe('when changing panel type', () => {
     describe('when changing panel type', () => {
       const newPanelPluginDefaults = {
       const newPanelPluginDefaults = {
         showThresholdLabels: false,
         showThresholdLabels: false,
@@ -180,7 +165,7 @@ describe('PanelModel', () => {
       it('should call react onPanelTypeChanged', () => {
       it('should call react onPanelTypeChanged', () => {
         expect(onPanelTypeChanged.mock.calls.length).toBe(1);
         expect(onPanelTypeChanged.mock.calls.length).toBe(1);
         expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
         expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
-        expect(onPanelTypeChanged.mock.calls[0][2].fieldOptions.thresholds).toBeDefined();
+        expect(onPanelTypeChanged.mock.calls[0][2].fieldOptions).toBeDefined();
       });
       });
 
 
       it('getQueryRunner() should return same instance after changing to another react panel', () => {
       it('getQueryRunner() should return same instance after changing to another react panel', () => {

+ 0 - 11
public/app/features/dashboard/state/PanelModel.ts

@@ -136,7 +136,6 @@ export class PanelModel {
 
 
     // queries must have refId
     // queries must have refId
     this.ensureQueryIds();
     this.ensureQueryIds();
-    this.restoreInfintyForThresholds();
   }
   }
 
 
   ensureQueryIds() {
   ensureQueryIds() {
@@ -149,16 +148,6 @@ export class PanelModel {
     }
     }
   }
   }
 
 
-  restoreInfintyForThresholds() {
-    if (this.options && this.options.fieldOptions) {
-      for (const threshold of this.options.fieldOptions.thresholds) {
-        if (threshold.value === null) {
-          threshold.value = -Infinity;
-        }
-      }
-    }
-  }
-
   getOptions() {
   getOptions() {
     return this.options;
     return this.options;
   }
   }

+ 61 - 0
public/app/plugins/panel/bargauge/BarGaugeMigrations.test.ts

@@ -0,0 +1,61 @@
+import { PanelModel } from '@grafana/ui';
+import { barGaugePanelMigrationCheck } from './BarGaugeMigrations';
+
+describe('BarGauge Panel Migrations', () => {
+  it('from 6.2', () => {
+    const panel = {
+      id: 7,
+      links: [],
+      options: {
+        displayMode: 'lcd',
+        fieldOptions: {
+          calcs: ['mean'],
+          defaults: {
+            decimals: null,
+            max: -22,
+            min: 33,
+            unit: 'watt',
+          },
+          mappings: [],
+          override: {},
+          thresholds: [
+            {
+              color: 'green',
+              index: 0,
+              value: null,
+            },
+            {
+              color: 'orange',
+              index: 1,
+              value: 40,
+            },
+            {
+              color: 'red',
+              index: 2,
+              value: 80,
+            },
+          ],
+          values: false,
+        },
+        orientation: 'vertical',
+      },
+      pluginVersion: '6.2.0',
+      targets: [
+        {
+          refId: 'A',
+          scenarioId: 'random_walk',
+        },
+        {
+          refId: 'B',
+          scenarioId: 'random_walk',
+        },
+      ],
+      timeFrom: null,
+      timeShift: null,
+      title: 'Usage',
+      type: 'bargauge',
+    } as PanelModel;
+
+    expect(barGaugePanelMigrationCheck(panel)).toMatchSnapshot();
+  });
+});

+ 36 - 0
public/app/plugins/panel/bargauge/BarGaugeMigrations.ts

@@ -0,0 +1,36 @@
+import { PanelModel } from '@grafana/ui';
+import {
+  sharedSingleStatMigrationCheck,
+  migrateOldThresholds,
+} from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
+import { BarGaugeOptions } from './types';
+
+export const barGaugePanelMigrationCheck = (panel: PanelModel<BarGaugeOptions>): Partial<BarGaugeOptions> => {
+  if (!panel.options) {
+    // This happens on the first load or when migrating from angular
+    return {};
+  }
+
+  // Move thresholds to field
+  const previousVersion = panel.pluginVersion || '';
+  if (previousVersion.startsWith('6.2') || previousVersion.startsWith('6.3')) {
+    console.log('TRANSFORM', panel.options);
+    const old = panel.options as any;
+    const { fieldOptions } = old;
+    if (fieldOptions) {
+      const { mappings, thresholds, ...rest } = fieldOptions;
+      rest.defaults = {
+        mappings,
+        thresholds: migrateOldThresholds(thresholds),
+        ...rest.defaults,
+      };
+      return {
+        ...old,
+        fieldOptions: rest,
+      };
+    }
+  }
+
+  // Default to the standard migration path
+  return sharedSingleStatMigrationCheck(panel);
+};

+ 1 - 2
public/app/plugins/panel/bargauge/BarGaugePanel.tsx

@@ -14,7 +14,6 @@ import { PanelProps } from '@grafana/ui';
 export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
 export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
   renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
   renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
     const { options } = this.props;
     const { options } = this.props;
-    const { fieldOptions } = options;
     const { field, display } = value;
     const { field, display } = value;
 
 
     return (
     return (
@@ -23,7 +22,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
         width={width}
         width={width}
         height={height}
         height={height}
         orientation={options.orientation}
         orientation={options.orientation}
-        thresholds={fieldOptions.thresholds}
+        thresholds={field.thresholds}
         theme={config.theme}
         theme={config.theme}
         itemSpacing={this.getItemSpacing()}
         itemSpacing={this.getItemSpacing()}
         displayMode={options.displayMode}
         displayMode={options.displayMode}

+ 14 - 9
public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx

@@ -19,17 +19,21 @@ import { Threshold, ValueMapping } from '@grafana/data';
 import { BarGaugeOptions, orientationOptions, displayModes } from './types';
 import { BarGaugeOptions, orientationOptions, displayModes } from './types';
 
 
 export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
 export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
-  onThresholdsChanged = (thresholds: Threshold[]) =>
-    this.onDisplayOptionsChanged({
-      ...this.props.options.fieldOptions,
+  onThresholdsChanged = (thresholds: Threshold[]) => {
+    const current = this.props.options.fieldOptions.defaults;
+    this.onDefaultsChange({
+      ...current,
       thresholds,
       thresholds,
     });
     });
+  };
 
 
-  onValueMappingsChanged = (mappings: ValueMapping[]) =>
-    this.onDisplayOptionsChanged({
-      ...this.props.options.fieldOptions,
+  onValueMappingsChanged = (mappings: ValueMapping[]) => {
+    const current = this.props.options.fieldOptions.defaults;
+    this.onDefaultsChange({
+      ...current,
       mappings,
       mappings,
     });
     });
+  };
 
 
   onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
   onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
     this.props.onOptionsChange({
     this.props.onOptionsChange({
@@ -50,6 +54,7 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
   render() {
   render() {
     const { options } = this.props;
     const { options } = this.props;
     const { fieldOptions } = options;
     const { fieldOptions } = options;
+    const { defaults } = fieldOptions;
 
 
     const labelWidth = 6;
     const labelWidth = 6;
 
 
@@ -80,13 +85,13 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
             </div>
             </div>
           </PanelOptionsGroup>
           </PanelOptionsGroup>
           <PanelOptionsGroup title="Field">
           <PanelOptionsGroup title="Field">
-            <FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={fieldOptions.defaults} />
+            <FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
           </PanelOptionsGroup>
           </PanelOptionsGroup>
 
 
-          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
+          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
         </PanelOptionsGrid>
         </PanelOptionsGrid>
 
 
-        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
       </>
       </>
     );
     );
   }
   }

+ 36 - 0
public/app/plugins/panel/bargauge/__snapshots__/BarGaugeMigrations.test.ts.snap

@@ -0,0 +1,36 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BarGauge Panel Migrations from 6.2 1`] = `
+Object {
+  "displayMode": "lcd",
+  "fieldOptions": Object {
+    "calcs": Array [
+      "mean",
+    ],
+    "defaults": Object {
+      "decimals": null,
+      "mappings": Array [],
+      "max": -22,
+      "min": 33,
+      "thresholds": Array [
+        Object {
+          "color": "green",
+          "value": -Infinity,
+        },
+        Object {
+          "color": "orange",
+          "value": 40,
+        },
+        Object {
+          "color": "red",
+          "value": 80,
+        },
+      ],
+      "unit": "watt",
+    },
+    "override": Object {},
+    "values": false,
+  },
+  "orientation": "vertical",
+}
+`;

+ 3 - 1
public/app/plugins/panel/bargauge/module.tsx

@@ -2,8 +2,10 @@ import { PanelPlugin, sharedSingleStatOptionsCheck } from '@grafana/ui';
 import { BarGaugePanel } from './BarGaugePanel';
 import { BarGaugePanel } from './BarGaugePanel';
 import { BarGaugePanelEditor } from './BarGaugePanelEditor';
 import { BarGaugePanelEditor } from './BarGaugePanelEditor';
 import { BarGaugeOptions, defaults } from './types';
 import { BarGaugeOptions, defaults } from './types';
+import { barGaugePanelMigrationCheck } from './BarGaugeMigrations';
 
 
 export const plugin = new PanelPlugin<BarGaugeOptions>(BarGaugePanel)
 export const plugin = new PanelPlugin<BarGaugeOptions>(BarGaugePanel)
   .setDefaults(defaults)
   .setDefaults(defaults)
   .setEditor(BarGaugePanelEditor)
   .setEditor(BarGaugePanelEditor)
-  .setPanelChangeHandler(sharedSingleStatOptionsCheck);
+  .setPanelChangeHandler(sharedSingleStatOptionsCheck)
+  .setMigrationHandler(barGaugePanelMigrationCheck);

+ 29 - 12
public/app/plugins/panel/gauge/GaugeMigrations.ts

@@ -1,7 +1,10 @@
 import { Field, getFieldReducers } from '@grafana/data';
 import { Field, getFieldReducers } from '@grafana/data';
 import { PanelModel } from '@grafana/ui';
 import { PanelModel } from '@grafana/ui';
 import { GaugeOptions } from './types';
 import { GaugeOptions } from './types';
-import { sharedSingleStatMigrationCheck } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
+import {
+  sharedSingleStatMigrationCheck,
+  migrateOldThresholds,
+} from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
 import { FieldDisplayOptions } from '@grafana/ui/src/utils/fieldDisplay';
 import { FieldDisplayOptions } from '@grafana/ui/src/utils/fieldDisplay';
 
 
 export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Partial<GaugeOptions> => {
 export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Partial<GaugeOptions> => {
@@ -10,7 +13,8 @@ export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Parti
     return {};
     return {};
   }
   }
 
 
-  if (!panel.pluginVersion || panel.pluginVersion.startsWith('6.1')) {
+  const previousVersion = panel.pluginVersion || '';
+  if (!previousVersion || previousVersion.startsWith('6.1')) {
     const old = panel.options as any;
     const old = panel.options as any;
     const { valueOptions } = old;
     const { valueOptions } = old;
 
 
@@ -20,23 +24,36 @@ export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Parti
     options.orientation = old.orientation;
     options.orientation = old.orientation;
 
 
     const fieldOptions = (options.fieldOptions = {} as FieldDisplayOptions);
     const fieldOptions = (options.fieldOptions = {} as FieldDisplayOptions);
-    fieldOptions.mappings = old.valueMappings;
-    fieldOptions.thresholds = old.thresholds;
 
 
     const field = (fieldOptions.defaults = {} as Field);
     const field = (fieldOptions.defaults = {} as Field);
-    if (valueOptions) {
-      field.unit = valueOptions.unit;
-      field.decimals = valueOptions.decimals;
-
-      // Make sure the stats have a valid name
-      if (valueOptions.stat) {
-        fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
-      }
+    field.mappings = old.valueMappings;
+    field.thresholds = migrateOldThresholds(old.thresholds);
+    field.unit = valueOptions.unit;
+    field.decimals = valueOptions.decimals;
+
+    // Make sure the stats have a valid name
+    if (valueOptions.stat) {
+      fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
     }
     }
     field.min = old.minValue;
     field.min = old.minValue;
     field.max = old.maxValue;
     field.max = old.maxValue;
 
 
     return options;
     return options;
+  } else if (previousVersion.startsWith('6.2') || previousVersion.startsWith('6.3')) {
+    const old = panel.options as any;
+    const { fieldOptions } = old;
+    if (fieldOptions) {
+      const { mappings, thresholds, ...rest } = fieldOptions;
+      rest.default = {
+        mappings,
+        thresholds: migrateOldThresholds(thresholds),
+        ...rest.defaults,
+      };
+      return {
+        ...old.options,
+        fieldOptions: rest,
+      };
+    }
   }
   }
 
 
   // Default to the standard migration path
   // Default to the standard migration path

+ 1 - 2
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -14,7 +14,6 @@ import { PanelProps, VizRepeater } from '@grafana/ui';
 export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
 export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
   renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
   renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
     const { options } = this.props;
     const { options } = this.props;
-    const { fieldOptions } = options;
     const { field, display } = value;
     const { field, display } = value;
 
 
     return (
     return (
@@ -22,7 +21,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
         value={display}
         value={display}
         width={width}
         width={width}
         height={height}
         height={height}
-        thresholds={fieldOptions.thresholds}
+        thresholds={field.thresholds}
         showThresholdLabels={options.showThresholdLabels}
         showThresholdLabels={options.showThresholdLabels}
         showThresholdMarkers={options.showThresholdMarkers}
         showThresholdMarkers={options.showThresholdMarkers}
         minValue={field.min}
         minValue={field.min}

+ 14 - 9
public/app/plugins/panel/gauge/GaugePanelEditor.tsx

@@ -27,17 +27,21 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
       showThresholdMarkers: !this.props.options.showThresholdMarkers,
       showThresholdMarkers: !this.props.options.showThresholdMarkers,
     });
     });
 
 
-  onThresholdsChanged = (thresholds: Threshold[]) =>
-    this.onDisplayOptionsChanged({
-      ...this.props.options.fieldOptions,
+  onThresholdsChanged = (thresholds: Threshold[]) => {
+    const current = this.props.options.fieldOptions.defaults;
+    this.onDefaultsChange({
+      ...current,
       thresholds,
       thresholds,
     });
     });
+  };
 
 
-  onValueMappingsChanged = (mappings: ValueMapping[]) =>
-    this.onDisplayOptionsChanged({
-      ...this.props.options.fieldOptions,
+  onValueMappingsChanged = (mappings: ValueMapping[]) => {
+    const current = this.props.options.fieldOptions.defaults;
+    this.onDefaultsChange({
+      ...current,
       mappings,
       mappings,
     });
     });
+  };
 
 
   onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
   onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
     this.props.onOptionsChange({
     this.props.onOptionsChange({
@@ -55,6 +59,7 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
   render() {
   render() {
     const { options } = this.props;
     const { options } = this.props;
     const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
     const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
+    const { defaults } = fieldOptions;
 
 
     return (
     return (
       <>
       <>
@@ -80,13 +85,13 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
           </PanelOptionsGroup>
           </PanelOptionsGroup>
 
 
           <PanelOptionsGroup title="Field">
           <PanelOptionsGroup title="Field">
-            <FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={fieldOptions.defaults} />
+            <FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
           </PanelOptionsGroup>
           </PanelOptionsGroup>
 
 
-          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
+          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
         </PanelOptionsGrid>
         </PanelOptionsGrid>
 
 
-        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
       </>
       </>
     );
     );
   }
   }

+ 29 - 33
public/app/plugins/panel/gauge/__snapshots__/GaugeMigrations.test.ts.snap

@@ -8,43 +8,39 @@ Object {
     ],
     ],
     "defaults": Object {
     "defaults": Object {
       "decimals": 3,
       "decimals": 3,
+      "mappings": Array [
+        Object {
+          "from": "50",
+          "id": 1,
+          "operator": "",
+          "text": "BIG",
+          "to": "1000",
+          "type": 2,
+          "value": "",
+        },
+      ],
       "max": "50",
       "max": "50",
       "min": "-50",
       "min": "-50",
+      "thresholds": Array [
+        Object {
+          "color": "green",
+          "value": -Infinity,
+        },
+        Object {
+          "color": "#EAB839",
+          "value": -25,
+        },
+        Object {
+          "color": "#6ED0E0",
+          "value": 0,
+        },
+        Object {
+          "color": "red",
+          "value": 25,
+        },
+      ],
       "unit": "accMS2",
       "unit": "accMS2",
     },
     },
-    "mappings": Array [
-      Object {
-        "from": "50",
-        "id": 1,
-        "operator": "",
-        "text": "BIG",
-        "to": "1000",
-        "type": 2,
-        "value": "",
-      },
-    ],
-    "thresholds": Array [
-      Object {
-        "color": "green",
-        "index": 0,
-        "value": null,
-      },
-      Object {
-        "color": "#EAB839",
-        "index": 1,
-        "value": -25,
-      },
-      Object {
-        "color": "#6ED0E0",
-        "index": 2,
-        "value": 0,
-      },
-      Object {
-        "color": "red",
-        "index": 3,
-        "value": 25,
-      },
-    ],
   },
   },
   "orientation": "auto",
   "orientation": "auto",
   "showThresholdLabels": true,
   "showThresholdLabels": true,

+ 8 - 5
public/app/plugins/panel/piechart/PieChartPanelEditor.tsx

@@ -14,11 +14,13 @@ import { PieChartOptionsBox } from './PieChartOptionsBox';
 import { PieChartOptions } from './types';
 import { PieChartOptions } from './types';
 
 
 export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
 export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
-  onValueMappingsChanged = (mappings: ValueMapping[]) =>
-    this.onDisplayOptionsChanged({
-      ...this.props.options.fieldOptions,
+  onValueMappingsChanged = (mappings: ValueMapping[]) => {
+    const current = this.props.options.fieldOptions.defaults;
+    this.onDefaultsChange({
+      ...current,
       mappings,
       mappings,
     });
     });
+  };
 
 
   onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
   onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
     this.props.onOptionsChange({
     this.props.onOptionsChange({
@@ -36,6 +38,7 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
   render() {
   render() {
     const { onOptionsChange, options } = this.props;
     const { onOptionsChange, options } = this.props;
     const { fieldOptions } = options;
     const { fieldOptions } = options;
+    const { defaults } = fieldOptions;
 
 
     return (
     return (
       <>
       <>
@@ -45,13 +48,13 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
           </PanelOptionsGroup>
           </PanelOptionsGroup>
 
 
           <PanelOptionsGroup title="Field (default)">
           <PanelOptionsGroup title="Field (default)">
-            <FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={fieldOptions.defaults} />
+            <FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
           </PanelOptionsGroup>
           </PanelOptionsGroup>
 
 
           <PieChartOptionsBox onOptionsChange={onOptionsChange} options={options} />
           <PieChartOptionsBox onOptionsChange={onOptionsChange} options={options} />
         </PanelOptionsGrid>
         </PanelOptionsGrid>
 
 
-        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
       </>
       </>
     );
     );
   }
   }

+ 14 - 9
public/app/plugins/panel/singlestat2/SingleStatEditor.tsx

@@ -18,17 +18,21 @@ import { FontSizeEditor } from './FontSizeEditor';
 import { SparklineEditor } from './SparklineEditor';
 import { SparklineEditor } from './SparklineEditor';
 
 
 export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
 export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
-  onThresholdsChanged = (thresholds: Threshold[]) =>
-    this.onDisplayOptionsChanged({
-      ...this.props.options.fieldOptions,
+  onThresholdsChanged = (thresholds: Threshold[]) => {
+    const current = this.props.options.fieldOptions.defaults;
+    this.onDefaultsChange({
+      ...current,
       thresholds,
       thresholds,
     });
     });
+  };
 
 
-  onValueMappingsChanged = (mappings: ValueMapping[]) =>
-    this.onDisplayOptionsChanged({
-      ...this.props.options.fieldOptions,
+  onValueMappingsChanged = (mappings: ValueMapping[]) => {
+    const current = this.props.options.fieldOptions.defaults;
+    this.onDefaultsChange({
+      ...current,
       mappings,
       mappings,
     });
     });
+  };
 
 
   onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
   onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
     this.props.onOptionsChange({
     this.props.onOptionsChange({
@@ -52,6 +56,7 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
   render() {
   render() {
     const { options } = this.props;
     const { options } = this.props;
     const { fieldOptions } = options;
     const { fieldOptions } = options;
+    const { defaults } = fieldOptions;
 
 
     return (
     return (
       <>
       <>
@@ -61,17 +66,17 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
           </PanelOptionsGroup>
           </PanelOptionsGroup>
 
 
           <PanelOptionsGroup title="Field (default)">
           <PanelOptionsGroup title="Field (default)">
-            <FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={fieldOptions.defaults} />
+            <FieldPropertiesEditor showMinMax={true} onChange={this.onDefaultsChange} value={defaults} />
           </PanelOptionsGroup>
           </PanelOptionsGroup>
 
 
           <FontSizeEditor options={options} onChange={this.props.onOptionsChange} />
           <FontSizeEditor options={options} onChange={this.props.onOptionsChange} />
           <ColoringEditor options={options} onChange={this.props.onOptionsChange} />
           <ColoringEditor options={options} onChange={this.props.onOptionsChange} />
           <SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} />
           <SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} />
 
 
-          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
+          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
         </PanelOptionsGrid>
         </PanelOptionsGrid>
 
 
-        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
       </>
       </>
     );
     );
   }
   }

+ 8 - 3
public/app/plugins/panel/singlestat2/types.ts

@@ -25,10 +25,15 @@ export interface SingleStatOptions extends SingleStatBaseOptions {
 export const standardFieldDisplayOptions: FieldDisplayOptions = {
 export const standardFieldDisplayOptions: FieldDisplayOptions = {
   values: false,
   values: false,
   calcs: [ReducerID.mean],
   calcs: [ReducerID.mean],
-  defaults: {},
+  defaults: {
+    min: 0,
+    max: 100,
+    thresholds: [
+      { value: -Infinity, color: 'green' },
+      { value: 80, color: 'red' }, // 80%
+    ],
+  },
   override: {},
   override: {},
-  mappings: [],
-  thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
 };
 };
 
 
 export const defaults: SingleStatOptions = {
 export const defaults: SingleStatOptions = {