Kaynağa Gözat

FieldDisplay: shared options model for singlestat panels (#16703)

* update single stat data model

* update single stat data model

* update single stat data model

* show limit default

* merge master

* change stat selector to single until #15954

* add tooltip

* begin children

* move options under display

* align gauge options

* add migration tests

* Docs: Updated changelog

* SingleStatPanels: show title if manual specified

* FieldPropEditor: Max should change max

* change stats to calcs in config

* remove prefix/suffix

* add test

* abort field cycle when passed the limit

* stub a better test

* move title to Field

* remove title
Ryan McKinley 6 yıl önce
ebeveyn
işleme
073c84179f
32 değiştirilmiş dosya ile 1154 ekleme ve 562 silme
  1. 4 0
      CHANGELOG.md
  2. 98 0
      packages/grafana-ui/src/components/SingleStatShared/FieldDisplayEditor.tsx
  3. 112 0
      packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx
  4. 62 0
      packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts
  5. 0 91
      packages/grafana-ui/src/components/SingleStatShared/SingleStatValueEditor.tsx
  6. 8 0
      packages/grafana-ui/src/components/SingleStatShared/index.ts
  7. 0 131
      packages/grafana-ui/src/components/SingleStatShared/shared.ts
  8. 1 1
      packages/grafana-ui/src/components/index.ts
  9. 5 0
      packages/grafana-ui/src/types/data.ts
  10. 8 19
      packages/grafana-ui/src/utils/displayValue.test.ts
  11. 10 19
      packages/grafana-ui/src/utils/displayValue.ts
  12. 132 0
      packages/grafana-ui/src/utils/fieldDisplay.test.ts
  13. 278 0
      packages/grafana-ui/src/utils/fieldDisplay.ts
  14. 1 0
      packages/grafana-ui/src/utils/index.ts
  15. 15 0
      packages/grafana-ui/src/utils/string.ts
  16. 11 7
      public/app/plugins/panel/bargauge/BarGaugePanel.tsx
  17. 36 23
      public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
  18. 3 14
      public/app/plugins/panel/bargauge/types.ts
  19. 82 0
      public/app/plugins/panel/gauge/GaugeMigrations.test.ts
  20. 43 0
      public/app/plugins/panel/gauge/GaugeMigrations.ts
  21. 0 50
      public/app/plugins/panel/gauge/GaugeOptionsBox.tsx
  22. 12 10
      public/app/plugins/panel/gauge/GaugePanel.tsx
  23. 60 15
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  24. 53 0
      public/app/plugins/panel/gauge/__snapshots__/GaugeMigrations.test.ts.snap
  25. 12 14
      public/app/plugins/panel/gauge/types.ts
  26. 3 1
      public/app/plugins/panel/graph2/getGraphSeriesModel.ts
  27. 4 6
      public/app/plugins/panel/piechart/PieChartPanel.tsx
  28. 28 10
      public/app/plugins/panel/piechart/PieChartPanelEditor.tsx
  29. 8 8
      public/app/plugins/panel/piechart/types.ts
  30. 31 13
      public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
  31. 23 119
      public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
  32. 11 11
      public/app/plugins/panel/singlestat2/types.ts

+ 4 - 0
CHANGELOG.md

@@ -1,5 +1,9 @@
 # 6.2.0 (unreleased)
 
+### Breaking changes
+
+* **Gauge Panel**: The suffix / prefix options have been removed from the new Guage Panel (introduced in v6.0). [#16870](https://github.com/grafana/grafana/issues/16870). 
+
 # 6.1.6 (2019-04-29)
 ### Features / Enhancements
 * **Security**: Bump jQuery to 3.4.0 . [#16761](https://github.com/grafana/grafana/pull/16761), [@dprokop](https://github.com/dprokop)

+ 98 - 0
packages/grafana-ui/src/components/SingleStatShared/FieldDisplayEditor.tsx

@@ -0,0 +1,98 @@
+// Libraries
+import React, { PureComponent, ChangeEvent } from 'react';
+
+// Components
+import { FormField, FormLabel, PanelOptionsGroup, StatsPicker, ReducerID } from '@grafana/ui';
+
+// Types
+import { FieldDisplayOptions, DEFAULT_FIELD_DISPLAY_VALUES_LIMIT } from '../../utils/fieldDisplay';
+import { Field } from '../../types/data';
+import Select, { SelectOptionItem } from '../Select/Select';
+import { toNumberString, toIntegerOrUndefined } from '../../utils';
+
+const showOptions: Array<SelectOptionItem<boolean>> = [
+  {
+    value: true,
+    label: 'All Values',
+    description: 'Each row in the response data',
+  },
+  {
+    value: false,
+    label: 'Calculation',
+    description: 'Calculate a value based on the response',
+  },
+];
+
+export interface Props {
+  options: FieldDisplayOptions;
+  onChange: (valueOptions: FieldDisplayOptions) => void;
+  labelWidth?: number;
+  children?: JSX.Element[];
+}
+
+export class FieldDisplayEditor extends PureComponent<Props> {
+  onShowValuesChange = (item: SelectOptionItem<boolean>) => {
+    const val = item.value === true;
+    this.props.onChange({ ...this.props.options, values: val });
+  };
+
+  onCalcsChange = (calcs: string[]) => {
+    this.props.onChange({ ...this.props.options, calcs });
+  };
+
+  onDefaultsChange = (value: Partial<Field>) => {
+    this.props.onChange({ ...this.props.options, defaults: value });
+  };
+
+  onLimitChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.props.onChange({
+      ...this.props.options,
+      limit: toIntegerOrUndefined(event.target.value),
+    });
+  };
+
+  render() {
+    const { options, children } = this.props;
+    const { calcs, values, limit } = options;
+
+    const labelWidth = this.props.labelWidth || 5;
+
+    return (
+      <PanelOptionsGroup title="Display">
+        <>
+          <div className="gf-form">
+            <FormLabel width={labelWidth}>Show</FormLabel>
+            <Select
+              options={showOptions}
+              value={values ? showOptions[0] : showOptions[1]}
+              onChange={this.onShowValuesChange}
+            />
+          </div>
+          {values ? (
+            <FormField
+              label="Limit"
+              labelWidth={labelWidth}
+              placeholder={`${DEFAULT_FIELD_DISPLAY_VALUES_LIMIT}`}
+              onChange={this.onLimitChange}
+              value={toNumberString(limit)}
+              type="number"
+            />
+          ) : (
+            <div className="gf-form">
+              <FormLabel width={labelWidth}>Calc</FormLabel>
+              <StatsPicker
+                width={12}
+                placeholder="Choose Stat"
+                defaultStat={ReducerID.mean}
+                allowMultiple={false}
+                stats={calcs}
+                onChange={this.onCalcsChange}
+              />
+            </div>
+          )}
+          {children}
+        </>
+      </PanelOptionsGroup>
+    );
+  }
+}

+ 112 - 0
packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx

@@ -0,0 +1,112 @@
+// Libraries
+import React, { PureComponent, ChangeEvent } from 'react';
+
+// Components
+import { FormField, FormLabel, PanelOptionsGroup, UnitPicker, SelectOptionItem } from '@grafana/ui';
+
+// Types
+import { Field } from '../../types/data';
+import { toNumberString, toIntegerOrUndefined } from '../../utils';
+
+import { VAR_SERIES_NAME, VAR_FIELD_NAME, VAR_CALC, VAR_CELL_PREFIX } from '../../utils/fieldDisplay';
+
+const labelWidth = 6;
+
+export interface Props {
+  title: string;
+  options: Partial<Field>;
+  onChange: (fieldProperties: Partial<Field>) => void;
+  showMinMax: boolean;
+}
+
+export class FieldPropertiesEditor extends PureComponent<Props> {
+  onTitleChange = (event: ChangeEvent<HTMLInputElement>) =>
+    this.props.onChange({ ...this.props.options, title: event.target.value });
+
+  // @ts-ignore
+  onUnitChange = (unit: SelectOptionItem<string>) => this.props.onChange({ ...this.props.value, unit: unit.value });
+
+  onDecimalChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.props.onChange({
+      ...this.props.options,
+      decimals: toIntegerOrUndefined(event.target.value),
+    });
+  };
+
+  onMinChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.props.onChange({
+      ...this.props.options,
+      min: toIntegerOrUndefined(event.target.value),
+    });
+  };
+
+  onMaxChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.props.onChange({
+      ...this.props.options,
+      max: toIntegerOrUndefined(event.target.value),
+    });
+  };
+
+  render() {
+    const { showMinMax, title } = this.props;
+    const { unit, decimals, min, max } = this.props.options;
+
+    const titleTooltip = (
+      <div>
+        Template Variables:
+        <br />
+        {'$' + VAR_SERIES_NAME}
+        <br />
+        {'$' + VAR_FIELD_NAME}
+        <br />
+        {'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
+      </div>
+    );
+
+    return (
+      <PanelOptionsGroup title={title}>
+        <>
+          <FormField
+            label="Title"
+            labelWidth={labelWidth}
+            onChange={this.onTitleChange}
+            value={this.props.options.title}
+            tooltip={titleTooltip}
+            placeholder="Auto"
+          />
+
+          <div className="gf-form">
+            <FormLabel width={labelWidth}>Unit</FormLabel>
+            <UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
+          </div>
+          {showMinMax && (
+            <>
+              <FormField
+                label="Min"
+                labelWidth={labelWidth}
+                onChange={this.onMinChange}
+                value={toNumberString(min)}
+                type="number"
+              />
+              <FormField
+                label="Max"
+                labelWidth={labelWidth}
+                onChange={this.onMaxChange}
+                value={toNumberString(max)}
+                type="number"
+              />
+            </>
+          )}
+          <FormField
+            label="Decimals"
+            labelWidth={labelWidth}
+            placeholder="auto"
+            onChange={this.onDecimalChange}
+            value={toNumberString(decimals)}
+            type="number"
+          />
+        </>
+      </PanelOptionsGroup>
+    );
+  }
+}

+ 62 - 0
packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts

@@ -0,0 +1,62 @@
+import cloneDeep from 'lodash/cloneDeep';
+import omit from 'lodash/omit';
+
+import { VizOrientation, PanelModel } from '../../types/panel';
+import { FieldDisplayOptions } from '../../utils/fieldDisplay';
+import { Field } from '../../types';
+import { getFieldReducers } from '../../utils/index';
+
+export interface SingleStatBaseOptions {
+  fieldOptions: FieldDisplayOptions;
+  orientation: VizOrientation;
+}
+
+const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
+
+export const sharedSingleStatOptionsCheck = (
+  options: Partial<SingleStatBaseOptions> | any,
+  prevPluginId: string,
+  prevOptions: any
+) => {
+  for (const k of optionsToKeep) {
+    if (prevOptions.hasOwnProperty(k)) {
+      options[k] = cloneDeep(prevOptions[k]);
+    }
+  }
+  return options;
+};
+
+export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseOptions>) => {
+  if (!panel.options) {
+    // This happens on the first load or when migrating from angular
+    return {};
+  }
+
+  // This migration aims to keep the most recent changes up-to-date
+  // Plugins should explicitly migrate for known version changes and only use this
+  // as a backup
+  const old = panel.options as any;
+  if (old.valueOptions) {
+    const { valueOptions } = old;
+
+    const fieldOptions = (old.fieldOptions = {} as FieldDisplayOptions);
+    fieldOptions.mappings = old.valueMappings;
+    fieldOptions.thresholds = old.thresholds;
+
+    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.min = old.minValue;
+    field.max = old.maxValue;
+
+    return omit(old, 'valueMappings', 'thresholds', 'valueOptions');
+  }
+  return panel.options;
+};

+ 0 - 91
packages/grafana-ui/src/components/SingleStatShared/SingleStatValueEditor.tsx

@@ -1,91 +0,0 @@
-// Libraries
-import React, { PureComponent, ChangeEvent } from 'react';
-
-// Components
-import {
-  FormField,
-  FormLabel,
-  PanelOptionsGroup,
-  StatsPicker,
-  UnitPicker,
-  ReducerID,
-  SelectOptionItem,
-} from '@grafana/ui';
-
-// Types
-import { SingleStatValueOptions } from './shared';
-
-const labelWidth = 6;
-
-export interface Props {
-  value: SingleStatValueOptions;
-  onChange: (valueOptions: SingleStatValueOptions) => void;
-}
-
-export class SingleStatValueEditor extends PureComponent<Props> {
-  // @ts-ignore
-  onUnitChange = (unit: SelectOptionItem<string>) => this.props.onChange({ ...this.props.value, unit: unit.value });
-
-  onStatsChange = (stats: string[]) => {
-    const stat = stats[0] || ReducerID.mean;
-    this.props.onChange({ ...this.props.value, stat });
-  };
-
-  onDecimalChange = (event: ChangeEvent<HTMLInputElement>) => {
-    if (!isNaN(parseInt(event.target.value, 10))) {
-      this.props.onChange({
-        ...this.props.value,
-        decimals: parseInt(event.target.value, 10),
-      });
-    } else {
-      this.props.onChange({
-        ...this.props.value,
-        decimals: null,
-      });
-    }
-  };
-
-  onPrefixChange = (event: ChangeEvent<HTMLInputElement>) =>
-    this.props.onChange({ ...this.props.value, prefix: event.target.value });
-  onSuffixChange = (event: ChangeEvent<HTMLInputElement>) =>
-    this.props.onChange({ ...this.props.value, suffix: event.target.value });
-
-  render() {
-    const { stat, unit, decimals, prefix, suffix } = this.props.value;
-
-    let decimalsString = '';
-    if (decimals !== null && decimals !== undefined && Number.isFinite(decimals as number)) {
-      decimalsString = decimals.toString();
-    }
-
-    return (
-      <PanelOptionsGroup title="Value">
-        <div className="gf-form">
-          <FormLabel width={labelWidth}>Show</FormLabel>
-          <StatsPicker
-            width={12}
-            placeholder="Choose Stat"
-            defaultStat={ReducerID.mean}
-            allowMultiple={false}
-            stats={[stat]}
-            onChange={this.onStatsChange}
-          />
-        </div>
-        <div className="gf-form">
-          <FormLabel width={labelWidth}>Unit</FormLabel>
-          <UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
-        </div>
-        <FormField
-          label="Decimals"
-          labelWidth={labelWidth}
-          placeholder="auto"
-          onChange={this.onDecimalChange}
-          value={decimalsString}
-          type="number"
-        />
-        <FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} />
-        <FormField label="Suffix" labelWidth={labelWidth} onChange={this.onSuffixChange} value={suffix || ''} />
-      </PanelOptionsGroup>
-    );
-  }
-}

+ 8 - 0
packages/grafana-ui/src/components/SingleStatShared/index.ts

@@ -0,0 +1,8 @@
+export { FieldDisplayEditor } from './FieldDisplayEditor';
+export { FieldPropertiesEditor } from './FieldPropertiesEditor';
+
+export {
+  SingleStatBaseOptions,
+  sharedSingleStatOptionsCheck,
+  sharedSingleStatMigrationCheck,
+} from './SingleStatBaseOptions';

+ 0 - 131
packages/grafana-ui/src/components/SingleStatShared/shared.ts

@@ -1,131 +0,0 @@
-import cloneDeep from 'lodash/cloneDeep';
-import {
-  ValueMapping,
-  Threshold,
-  VizOrientation,
-  PanelModel,
-  DisplayValue,
-  FieldType,
-  NullValueMode,
-  GrafanaTheme,
-  SeriesData,
-  InterpolateFunction,
-} from '../../types';
-import { getFieldReducers, reduceField } from '../../utils/fieldReducer';
-import { getDisplayProcessor } from '../../utils/displayValue';
-export { SingleStatValueEditor } from './SingleStatValueEditor';
-
-export interface SingleStatBaseOptions {
-  valueMappings: ValueMapping[];
-  thresholds: Threshold[];
-  valueOptions: SingleStatValueOptions;
-  orientation: VizOrientation;
-}
-
-export interface SingleStatValueOptions {
-  unit: string;
-  suffix: string;
-  stat: string;
-  prefix: string;
-  decimals?: number | null;
-}
-
-export interface GetSingleStatDisplayValueOptions {
-  data?: SeriesData[];
-  theme: GrafanaTheme;
-  valueMappings: ValueMapping[];
-  thresholds: Threshold[];
-  valueOptions: SingleStatValueOptions;
-  replaceVariables: InterpolateFunction;
-}
-
-export const getSingleStatDisplayValues = (options: GetSingleStatDisplayValueOptions): DisplayValue[] => {
-  const { data, replaceVariables, valueOptions } = options;
-  const { unit, decimals, stat } = valueOptions;
-
-  const display = getDisplayProcessor({
-    unit,
-    decimals,
-    mappings: options.valueMappings,
-    thresholds: options.thresholds,
-    prefix: replaceVariables(valueOptions.prefix),
-    suffix: replaceVariables(valueOptions.suffix),
-    theme: options.theme,
-  });
-
-  const values: DisplayValue[] = [];
-
-  if (data) {
-    for (const series of data) {
-      if (stat === 'name') {
-        values.push(display(series.name));
-      }
-
-      for (let i = 0; i < series.fields.length; i++) {
-        const column = series.fields[i];
-
-        // Show all fields that are not 'time'
-        if (column.type === FieldType.number) {
-          const stats = reduceField({
-            series,
-            fieldIndex: i,
-            reducers: [stat], // The stats to calculate
-            nullValueMode: NullValueMode.Null,
-          });
-
-          const displayValue = display(stats[stat]);
-          if (series.name) {
-            displayValue.title = replaceVariables(series.name);
-          }
-          values.push(displayValue);
-        }
-      }
-    }
-  }
-
-  if (values.length === 0) {
-    values.push({
-      numeric: 0,
-      text: 'No data',
-    });
-  } else if (values.length === 1) {
-    // Don't show title for single item
-    values[0].title = undefined;
-  }
-
-  return values;
-};
-
-const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
-
-export const sharedSingleStatOptionsCheck = (
-  options: Partial<SingleStatBaseOptions> | any,
-  prevPluginId: string,
-  prevOptions: any
-) => {
-  for (const k of optionsToKeep) {
-    if (prevOptions.hasOwnProperty(k)) {
-      options[k] = cloneDeep(prevOptions[k]);
-    }
-  }
-  return options;
-};
-
-export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseOptions>) => {
-  const options = panel.options;
-
-  if (!options) {
-    // This happens on the first load or when migrating from angular
-    return {};
-  }
-
-  if (options.valueOptions) {
-    // 6.1 renamed some stats, This makes sure they are up to date
-    // avg -> mean, current -> last, total -> sum
-    const { valueOptions } = options;
-    if (valueOptions && valueOptions.stat) {
-      valueOptions.stat = getFieldReducers([valueOptions.stat]).map(s => s.id)[0];
-    }
-  }
-  return options;
-};

+ 1 - 1
packages/grafana-ui/src/components/index.ts

@@ -51,5 +51,5 @@ export { LegendOptions, LegendBasicOptions, LegendRenderOptions, LegendList, Leg
 // Panel editors
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
 export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
-export * from './SingleStatShared/shared';
+export * from './SingleStatShared/index';
 export { CallToActionCard } from './CallToActionCard/CallToActionCard';

+ 5 - 0
packages/grafana-ui/src/types/data.ts

@@ -41,10 +41,15 @@ export interface QueryResultBase {
 
 export interface Field {
   name: string; // The column name
+  title?: string; // The display value for this field.  This supports template variables blank is auto
   type?: FieldType;
   filterable?: boolean;
   unit?: string;
   dateFormat?: string; // Source data format
+  decimals?: number | null; // Significant digits (for display)
+  color?: string;
+  min?: number | null;
+  max?: number | null;
 }
 
 export interface Labels {

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

@@ -18,10 +18,10 @@ describe('Process simple display values', () => {
     getDisplayProcessor(),
 
     // Add a simple option that is not used (uses a different base class)
-    getDisplayProcessor({ color: '#FFF' }),
+    getDisplayProcessor({ field: { color: '#FFF' } }),
 
     // Add a simple option that is not used (uses a different base class)
-    getDisplayProcessor({ unit: 'locale' }),
+    getDisplayProcessor({ field: { unit: 'locale' } }),
   ];
 
   it('support null', () => {
@@ -73,17 +73,6 @@ describe('Process simple display values', () => {
   });
 });
 
-describe('Processor with more configs', () => {
-  it('support prefix & suffix', () => {
-    const processor = getDisplayProcessor({
-      prefix: 'AA_',
-      suffix: '_ZZ',
-    });
-
-    expect(processor('XXX').text).toEqual('AA_XXX_ZZ');
-  });
-});
-
 describe('Get color from threshold', () => {
   it('should get first threshold color when only one threshold', () => {
     const thresholds = [{ index: 0, value: -Infinity, color: '#7EB26D' }];
@@ -124,7 +113,7 @@ describe('Format value', () => {
     const valueMappings: ValueMapping[] = [];
     const value = '6';
 
-    const instance = getDisplayProcessor({ mappings: valueMappings, decimals: 1 });
+    const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
 
     const result = instance(value);
 
@@ -137,7 +126,7 @@ describe('Format value', () => {
       { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
     ];
     const value = '10';
-    const instance = getDisplayProcessor({ mappings: valueMappings, decimals: 1 });
+    const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
 
     const result = instance(value);
 
@@ -146,21 +135,21 @@ describe('Format value', () => {
 
   it('should set auto decimals, 1 significant', () => {
     const value = '1.23';
-    const instance = getDisplayProcessor({ decimals: null });
+    const instance = getDisplayProcessor({ field: { decimals: null } });
 
     expect(instance(value).text).toEqual('1.2');
   });
 
   it('should set auto decimals, 2 significant', () => {
     const value = '0.0245';
-    const instance = getDisplayProcessor({ decimals: null });
+    const instance = getDisplayProcessor({ field: { decimals: null } });
 
     expect(instance(value).text).toEqual('0.02');
   });
 
   it('should use override decimals', () => {
     const value = 100030303;
-    const instance = getDisplayProcessor({ decimals: 2, unit: 'bytes' });
+    const instance = getDisplayProcessor({ field: { decimals: 2, unit: 'bytes' } });
     expect(instance(value).text).toEqual('95.40 MiB');
   });
 
@@ -170,7 +159,7 @@ describe('Format value', () => {
       { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
     ];
     const value = '11';
-    const instance = getDisplayProcessor({ mappings: valueMappings, decimals: 1 });
+    const instance = getDisplayProcessor({ mappings: valueMappings, field: { decimals: 1 } });
 
     expect(instance(value).text).toEqual('1-20');
   });

+ 10 - 19
packages/grafana-ui/src/utils/displayValue.ts

@@ -16,20 +16,16 @@ import {
   GrafanaTheme,
   GrafanaThemeType,
   DecimalCount,
+  Field,
 } from '../types';
 
 export type DisplayProcessor = (value: any) => DisplayValue;
 
 export interface DisplayValueOptions {
-  unit?: string;
-  decimals?: DecimalCount;
-  dateFormat?: string; // If set try to convert numbers to date
+  field?: Partial<Field>;
 
-  color?: string;
   mappings?: ValueMapping[];
   thresholds?: Threshold[];
-  prefix?: string;
-  suffix?: string;
 
   // Alternative to empty string
   noValue?: string;
@@ -41,11 +37,12 @@ export interface DisplayValueOptions {
 
 export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProcessor {
   if (options && !_.isEmpty(options)) {
-    const formatFunc = getValueFormat(options.unit || 'none');
+    const field = options.field ? options.field : {};
+    const formatFunc = getValueFormat(field.unit || 'none');
 
     return (value: any) => {
-      const { prefix, suffix, mappings, thresholds, theme } = options;
-      let color = options.color;
+      const { mappings, thresholds, theme } = options;
+      let color = field.color;
 
       let text = _.toString(value);
       let numeric = toNumber(value);
@@ -66,17 +63,17 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce
         }
       }
 
-      if (options.dateFormat) {
-        const date = toMoment(value, numeric, options.dateFormat);
+      if (field.dateFormat) {
+        const date = toMoment(value, numeric, field.dateFormat);
         if (date.isValid()) {
-          text = date.format(options.dateFormat);
+          text = date.format(field.dateFormat);
           shouldFormat = false;
         }
       }
 
       if (!isNaN(numeric)) {
         if (shouldFormat && !_.isBoolean(value)) {
-          const { decimals, scaledDecimals } = getDecimalsForValue(value, options.decimals);
+          const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals);
           text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
         }
         if (thresholds && thresholds.length > 0) {
@@ -87,12 +84,6 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce
       if (!text) {
         text = options.noValue ? options.noValue : '';
       }
-      if (prefix) {
-        text = prefix + text;
-      }
-      if (suffix) {
-        text = text + suffix;
-      }
       return { text, numeric, color };
     };
   }

+ 132 - 0
packages/grafana-ui/src/utils/fieldDisplay.test.ts

@@ -0,0 +1,132 @@
+import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
+import { FieldType } from '../types/data';
+import { ReducerID } from './fieldReducer';
+import { GrafanaThemeType } from '../types/theme';
+import { getTheme } from '../themes/index';
+
+describe('FieldDisplay', () => {
+  it('Construct simple field properties', () => {
+    const f0 = {
+      min: 0,
+      max: 100,
+      dateFormat: 'YYYY',
+    };
+    const f1 = {
+      unit: 'ms',
+      dateFormat: '', // should be ignored
+      max: parseFloat('NOPE'), // should be ignored
+      min: null,
+    };
+    let field = getFieldProperties(f0, f1);
+    expect(field.min).toEqual(0);
+    expect(field.max).toEqual(100);
+    expect(field.unit).toEqual('ms');
+    expect(field.dateFormat).toEqual('YYYY');
+
+    // last one overrieds
+    const f2 = {
+      unit: 'none', // ignore 'none'
+      max: -100, // lower than min! should flip min/max
+    };
+    field = getFieldProperties(f0, f1, f2);
+    expect(field.max).toEqual(0);
+    expect(field.min).toEqual(-100);
+    expect(field.unit).toEqual('ms');
+  });
+
+  // Simple test dataset
+  const options: GetFieldDisplayValuesOptions = {
+    data: [
+      {
+        name: 'Series Name',
+        fields: [
+          { name: 'Field 1', type: FieldType.string },
+          { name: 'Field 2', type: FieldType.number },
+          { name: 'Field 3', type: FieldType.number },
+        ],
+        rows: [
+          ['a', 1, 2], // 0
+          ['b', 3, 4], // 1
+          ['c', 5, 6], // 2
+        ],
+      },
+    ],
+    replaceVariables: (value: string) => {
+      return value; // Return it unchanged
+    },
+    fieldOptions: {
+      calcs: [],
+      mappings: [],
+      thresholds: [],
+      override: {},
+      defaults: {},
+    },
+    theme: getTheme(GrafanaThemeType.Dark),
+  };
+
+  it('show first numeric values', () => {
+    const display = getFieldDisplayValues({
+      ...options,
+      fieldOptions: {
+        calcs: [ReducerID.first],
+        mappings: [],
+        thresholds: [],
+        override: {},
+        defaults: {
+          title: '$__cell_0 * $__field_name * $__series_name',
+        },
+      },
+    });
+    expect(display.map(v => v.display.text)).toEqual(['1', '2']);
+    // expect(display.map(v => v.display.title)).toEqual([
+    //   'a * Field 1 * Series Name', // 0
+    //   'b * Field 2 * Series Name', // 1
+    // ]);
+  });
+
+  it('show last numeric values', () => {
+    const display = getFieldDisplayValues({
+      ...options,
+      fieldOptions: {
+        calcs: [ReducerID.last],
+        mappings: [],
+        thresholds: [],
+        override: {},
+        defaults: {},
+      },
+    });
+    expect(display.map(v => v.display.numeric)).toEqual([5, 6]);
+  });
+
+  it('show all numeric values', () => {
+    const display = getFieldDisplayValues({
+      ...options,
+      fieldOptions: {
+        values: true, //
+        limit: 1000,
+        calcs: [],
+        mappings: [],
+        thresholds: [],
+        override: {},
+        defaults: {},
+      },
+    });
+    expect(display.map(v => v.display.numeric)).toEqual([1, 3, 5, 2, 4, 6]);
+  });
+
+  it('show 2 numeric values (limit)', () => {
+    const display = getFieldDisplayValues({
+      ...options,
+      fieldOptions: {
+        values: true, //
+        limit: 2,
+        calcs: [],
+        mappings: [],
+        thresholds: [],
+        override: {},
+        defaults: {},
+      },
+    });
+    expect(display.map(v => v.display.numeric)).toEqual([1, 3]); // First 2 are from the first field
+  });
+});

+ 278 - 0
packages/grafana-ui/src/utils/fieldDisplay.ts

@@ -0,0 +1,278 @@
+import toNumber from 'lodash/toNumber';
+import toString from 'lodash/toString';
+
+import {
+  ValueMapping,
+  Threshold,
+  DisplayValue,
+  FieldType,
+  NullValueMode,
+  GrafanaTheme,
+  SeriesData,
+  InterpolateFunction,
+  Field,
+  ScopedVars,
+  GraphSeriesValue,
+} from '../types/index';
+import { getDisplayProcessor } from './displayValue';
+import { getFlotPairs } from './flotPairs';
+import { ReducerID, reduceField } from './fieldReducer';
+
+export interface FieldDisplayOptions {
+  values?: boolean; // If true show each row value
+  limit?: number; // if showing all values limit
+  calcs: string[]; // when !values, pick one value for the whole field
+
+  defaults: Partial<Field>; // Use these values unless otherwise stated
+  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_FIELD_NAME = '__field_name';
+export const VAR_CALC = '__calc';
+export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates
+
+function getTitleTemplate(title: string | undefined, stats: string[], data?: SeriesData[]): string {
+  // If the title exists, use it as a template variable
+  if (title) {
+    return title;
+  }
+  if (!data || !data.length) {
+    return 'No Data';
+  }
+
+  let fieldCount = 0;
+  for (const field of data[0].fields) {
+    if (field.type === FieldType.number) {
+      fieldCount++;
+    }
+  }
+
+  const parts: string[] = [];
+  if (stats.length > 1) {
+    parts.push('$' + VAR_CALC);
+  }
+  if (data.length > 1) {
+    parts.push('$' + VAR_SERIES_NAME);
+  }
+  if (fieldCount > 1 || !parts.length) {
+    parts.push('$' + VAR_FIELD_NAME);
+  }
+  return parts.join(' ');
+}
+
+export interface FieldDisplay {
+  field: Field;
+  display: DisplayValue;
+  sparkline?: GraphSeriesValue[][];
+}
+
+export interface GetFieldDisplayValuesOptions {
+  data?: SeriesData[];
+  fieldOptions: FieldDisplayOptions;
+  replaceVariables: InterpolateFunction;
+  sparkline?: boolean; // Calculate the sparkline
+  theme: GrafanaTheme;
+}
+
+export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
+
+export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
+  const { data, replaceVariables, fieldOptions, sparkline } = options;
+  const { defaults, override } = fieldOptions;
+  const calcs = fieldOptions.calcs.length ? fieldOptions.calcs : [ReducerID.last];
+
+  const values: FieldDisplay[] = [];
+
+  if (data) {
+    let hitLimit = false;
+    const limit = fieldOptions.limit ? fieldOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
+    const defaultTitle = getTitleTemplate(fieldOptions.defaults.title, calcs, data);
+    const scopedVars: ScopedVars = {};
+
+    for (let s = 0; s < data.length && !hitLimit; s++) {
+      let series = data[s];
+      if (!series.name) {
+        series = {
+          ...series,
+          name: series.refId ? series.refId : `Series[${s}]`,
+        };
+      }
+      scopedVars[VAR_SERIES_NAME] = { text: 'Series', value: series.name };
+
+      let timeColumn = -1;
+      if (sparkline) {
+        for (let i = 0; i < series.fields.length; i++) {
+          if (series.fields[i].type === FieldType.time) {
+            timeColumn = i;
+            break;
+          }
+        }
+      }
+
+      for (let i = 0; i < series.fields.length && !hitLimit; i++) {
+        const field = getFieldProperties(defaults, series.fields[i], override);
+
+        // Show all number fields
+        if (field.type !== FieldType.number) {
+          continue;
+        }
+
+        if (!field.name) {
+          field.name = `Field[${s}]`; // it is a copy, so safe to edit
+        }
+
+        scopedVars[VAR_FIELD_NAME] = { text: 'Field', value: field.name };
+
+        const display = getDisplayProcessor({
+          field,
+          mappings: fieldOptions.mappings,
+          thresholds: fieldOptions.thresholds,
+          theme: options.theme,
+        });
+
+        const title = field.title ? field.title : defaultTitle;
+
+        // Show all number fields
+        if (fieldOptions.values) {
+          const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
+
+          for (const row of series.rows) {
+            // Add all the row variables
+            if (usesCellValues) {
+              for (let j = 0; j < series.fields.length; j++) {
+                scopedVars[VAR_CELL_PREFIX + j] = {
+                  value: row[j],
+                  text: toString(row[j]),
+                };
+              }
+            }
+
+            const displayValue = display(row[i]);
+            displayValue.title = replaceVariables(title, scopedVars);
+            values.push({
+              field,
+              display: displayValue,
+            });
+
+            if (values.length >= limit) {
+              hitLimit = true;
+              break;
+            }
+          }
+        } else {
+          const results = reduceField({
+            series,
+            fieldIndex: i,
+            reducers: calcs, // The stats to calculate
+            nullValueMode: NullValueMode.Null,
+          });
+
+          // Single sparkline for a field
+          const points =
+            timeColumn < 0
+              ? undefined
+              : getFlotPairs({
+                  series,
+                  xIndex: timeColumn,
+                  yIndex: i,
+                  nullValueMode: NullValueMode.Null,
+                });
+
+          for (const calc of calcs) {
+            scopedVars[VAR_CALC] = { value: calc, text: calc };
+            const displayValue = display(results[calc]);
+            displayValue.title = replaceVariables(title, scopedVars);
+            values.push({
+              field,
+              display: displayValue,
+              sparkline: points,
+            });
+          }
+        }
+      }
+    }
+  }
+
+  if (values.length === 0) {
+    values.push({
+      field: { name: 'No Data' },
+      display: {
+        numeric: 0,
+        text: 'No data',
+      },
+    });
+  } else if (values.length === 1 && !fieldOptions.defaults.title) {
+    // Don't show title for single item
+    values[0].display.title = undefined;
+  }
+
+  return values;
+};
+
+const numericFieldProps: any = {
+  decimals: true,
+  min: true,
+  max: true,
+};
+
+/**
+ * Returns a version of the field with the overries applied.  Any property with
+ * value: null | undefined | empty string are skipped.
+ *
+ * For numeric values, only valid numbers will be applied
+ * for units, 'none' will be skipped
+ */
+export function applyFieldProperties(field: Field, props?: Partial<Field>): Field {
+  if (!props) {
+    return field;
+  }
+  const keys = Object.keys(props);
+  if (!keys.length) {
+    return field;
+  }
+  const copy = { ...field } as any; // make a copy that we will manipulate directly
+  for (const key of keys) {
+    const val = (props as any)[key];
+    if (val === null || val === undefined) {
+      continue;
+    }
+
+    if (numericFieldProps[key]) {
+      const num = toNumber(val);
+      if (!isNaN(num)) {
+        copy[key] = num;
+      }
+    } else if (val) {
+      // skips empty string
+      if (key === 'unit' && val === 'none') {
+        continue;
+      }
+      copy[key] = val;
+    }
+  }
+  return copy as Field;
+}
+
+type PartialField = Partial<Field>;
+
+export function getFieldProperties(...props: PartialField[]): Field {
+  let field = props[0] as Field;
+  for (let i = 1; i < props.length; i++) {
+    field = applyFieldProperties(field, props[i]);
+  }
+
+  // Verify that max > min
+  if (field.hasOwnProperty('min') && field.hasOwnProperty('max') && field.min! > field.max!) {
+    return {
+      ...field,
+      min: field.max,
+      max: field.min,
+    };
+  }
+  return field;
+}

+ 1 - 0
packages/grafana-ui/src/utils/index.ts

@@ -7,6 +7,7 @@ export * from './string';
 export * from './csv';
 export * from './fieldReducer';
 export * from './displayValue';
+export * from './fieldDisplay';
 export * from './deprecationWarning';
 export * from './logs';
 export * from './labels';

+ 15 - 0
packages/grafana-ui/src/utils/string.ts

@@ -49,3 +49,18 @@ export function getIntervalFromString(strInterval: string): SelectOptionItem<num
     value: stringToMs(strInterval),
   };
 }
+
+export function toNumberString(value: number | undefined | null): string {
+  if (value !== null && value !== undefined && Number.isFinite(value as number)) {
+    return value.toString();
+  }
+  return '';
+}
+
+export function toIntegerOrUndefined(value: string): number | undefined {
+  if (!value) {
+    return undefined;
+  }
+  const v = parseInt(value, 10);
+  return isNaN(v) ? undefined : v;
+}

+ 11 - 7
public/app/plugins/panel/bargauge/BarGaugePanel.tsx

@@ -5,33 +5,37 @@ import React, { PureComponent } from 'react';
 import { config } from 'app/core/config';
 
 // Components
-import { BarGauge, VizRepeater, getSingleStatDisplayValues } from '@grafana/ui/src/components';
+import { BarGauge, VizRepeater, getFieldDisplayValues, FieldDisplay } from '@grafana/ui';
 
 // Types
 import { BarGaugeOptions } from './types';
-import { PanelProps, DisplayValue } from '@grafana/ui/src/types';
+import { PanelProps } from '@grafana/ui/src/types';
 
 export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
-  renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
+  renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
     const { options } = this.props;
+    const { fieldOptions } = options;
+    const { field, display } = value;
 
     return (
       <BarGauge
-        value={value}
+        value={display}
         width={width}
         height={height}
         orientation={options.orientation}
-        thresholds={options.thresholds}
+        thresholds={fieldOptions.thresholds}
         theme={config.theme}
         itemSpacing={this.getItemSpacing()}
         displayMode={options.displayMode}
+        minValue={field.min}
+        maxValue={field.max}
       />
     );
   };
 
-  getValues = (): DisplayValue[] => {
+  getValues = (): FieldDisplay[] => {
     const { data, options, replaceVariables } = this.props;
-    return getSingleStatDisplayValues({
+    return getFieldDisplayValues({
       ...options,
       replaceVariables,
       theme: config.theme,

+ 36 - 23
public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx

@@ -6,10 +6,10 @@ import {
   ThresholdsEditor,
   ValueMappingsEditor,
   PanelOptionsGrid,
-  PanelOptionsGroup,
-  FormField,
-  SingleStatValueOptions,
-  SingleStatValueEditor,
+  FieldDisplayEditor,
+  FieldDisplayOptions,
+  Field,
+  FieldPropertiesEditor,
 } from '@grafana/ui';
 
 // Types
@@ -18,40 +18,45 @@ import { BarGaugeOptions, orientationOptions, displayModes } from './types';
 
 export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
   onThresholdsChanged = (thresholds: Threshold[]) =>
-    this.props.onOptionsChange({
-      ...this.props.options,
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
       thresholds,
     });
 
-  onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
-    this.props.onOptionsChange({
-      ...this.props.options,
-      valueMappings,
+  onValueMappingsChanged = (mappings: ValueMapping[]) =>
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
+      mappings,
     });
 
-  onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
+  onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
     this.props.onOptionsChange({
       ...this.props.options,
-      valueOptions,
+      fieldOptions,
     });
 
-  onMinValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, minValue: target.value });
-  onMaxValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, maxValue: target.value });
+  onDefaultsChange = (field: Partial<Field>) => {
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
+      defaults: field,
+    });
+  };
+
   onOrientationChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, orientation: value });
   onDisplayModeChange = ({ value }) => this.props.onOptionsChange({ ...this.props.options, displayMode: value });
 
   render() {
     const { options } = this.props;
+    const { fieldOptions } = options;
+
+    const labelWidth = 6;
 
     return (
       <>
         <PanelOptionsGrid>
-          <SingleStatValueEditor onChange={this.onValueOptionsChanged} value={options.valueOptions} />
-          <PanelOptionsGroup title="Gauge">
-            <FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={options.minValue} />
-            <FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={options.maxValue} />
+          <FieldDisplayEditor onChange={this.onDisplayOptionsChanged} options={fieldOptions} labelWidth={labelWidth}>
             <div className="form-field">
-              <FormLabel width={8}>Orientation</FormLabel>
+              <FormLabel width={labelWidth}>Orientation</FormLabel>
               <Select
                 width={12}
                 options={orientationOptions}
@@ -61,7 +66,7 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
               />
             </div>
             <div className="form-field">
-              <FormLabel width={8}>Display Mode</FormLabel>
+              <FormLabel width={labelWidth}>Mode</FormLabel>
               <Select
                 width={12}
                 options={displayModes}
@@ -70,11 +75,19 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
                 value={displayModes.find(item => item.value === options.displayMode)}
               />
             </div>
-          </PanelOptionsGroup>
-          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
+          </FieldDisplayEditor>
+
+          <FieldPropertiesEditor
+            title="Field"
+            showMinMax={true}
+            onChange={this.onDefaultsChange}
+            options={fieldOptions.defaults}
+          />
+
+          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
         </PanelOptionsGrid>
 
-        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
       </>
     );
   }

+ 3 - 14
public/app/plugins/panel/bargauge/types.ts

@@ -1,8 +1,7 @@
-import { VizOrientation, SelectOptionItem, ReducerID, SingleStatBaseOptions } from '@grafana/ui';
+import { VizOrientation, SelectOptionItem, SingleStatBaseOptions } from '@grafana/ui';
+import { standardGaugeFieldOptions } from '../gauge/types';
 
 export interface BarGaugeOptions extends SingleStatBaseOptions {
-  minValue: number;
-  maxValue: number;
   displayMode: 'basic' | 'lcd' | 'gradient';
 }
 
@@ -18,17 +17,7 @@ export const orientationOptions: Array<SelectOptionItem<VizOrientation>> = [
 ];
 
 export const defaults: BarGaugeOptions = {
-  minValue: 0,
-  maxValue: 100,
   displayMode: 'lcd',
   orientation: VizOrientation.Horizontal,
-  valueOptions: {
-    unit: 'none',
-    stat: ReducerID.mean,
-    prefix: '',
-    suffix: '',
-    decimals: null,
-  },
-  thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
-  valueMappings: [],
+  fieldOptions: standardGaugeFieldOptions,
 };

+ 82 - 0
public/app/plugins/panel/gauge/GaugeMigrations.test.ts

@@ -0,0 +1,82 @@
+import { PanelModel } from '@grafana/ui';
+import { gaugePanelMigrationCheck } from './GaugeMigrations';
+
+describe('Gauge Panel Migrations', () => {
+  it('from 6.1.1', () => {
+    const panel = {
+      datasource: '-- Grafana --',
+      gridPos: {
+        h: 9,
+        w: 12,
+        x: 0,
+        y: 0,
+      },
+      id: 2,
+      options: {
+        maxValue: '50',
+        minValue: '-50',
+        orientation: 'auto',
+        showThresholdLabels: true,
+        showThresholdMarkers: true,
+        thresholds: [
+          {
+            color: 'green',
+            index: 0,
+            value: null,
+          },
+          {
+            color: '#EAB839',
+            index: 1,
+            value: -25,
+          },
+          {
+            color: '#6ED0E0',
+            index: 2,
+            value: 0,
+          },
+          {
+            color: 'red',
+            index: 3,
+            value: 25,
+          },
+        ],
+        valueMappings: [
+          {
+            id: 1,
+            operator: '',
+            value: '',
+            text: 'BIG',
+            type: 2,
+            from: '50',
+            to: '1000',
+          },
+        ],
+        valueOptions: {
+          decimals: 3,
+          prefix: 'XX',
+          stat: 'last',
+          suffix: 'YY',
+          unit: 'accMS2',
+        },
+      },
+      pluginVersion: '6.1.6',
+      targets: [
+        {
+          refId: 'A',
+        },
+        {
+          refId: 'B',
+        },
+        {
+          refId: 'C',
+        },
+      ],
+      timeFrom: null,
+      timeShift: null,
+      title: 'Panel Title',
+      type: 'gauge',
+    } as PanelModel;
+
+    expect(gaugePanelMigrationCheck(panel)).toMatchSnapshot();
+  });
+});

+ 43 - 0
public/app/plugins/panel/gauge/GaugeMigrations.ts

@@ -0,0 +1,43 @@
+import { PanelModel, Field, getFieldReducers } from '@grafana/ui';
+import { GaugeOptions } from './types';
+import { sharedSingleStatMigrationCheck } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
+import { FieldDisplayOptions } from '@grafana/ui/src/utils/fieldDisplay';
+
+export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Partial<GaugeOptions> => {
+  if (!panel.options) {
+    // This happens on the first load or when migrating from angular
+    return {};
+  }
+
+  if (!panel.pluginVersion || panel.pluginVersion.startsWith('6.1')) {
+    const old = panel.options as any;
+    const { valueOptions } = old;
+
+    const options = {} as GaugeOptions;
+    options.showThresholdLabels = old.showThresholdLabels;
+    options.showThresholdMarkers = old.showThresholdMarkers;
+    options.orientation = old.orientation;
+
+    const fieldOptions = (options.fieldOptions = {} as FieldDisplayOptions);
+    fieldOptions.mappings = old.valueMappings;
+    fieldOptions.thresholds = old.thresholds;
+
+    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.min = old.minValue;
+    field.max = old.maxValue;
+
+    return options;
+  }
+
+  // Default to the standard migration path
+  return sharedSingleStatMigrationCheck(panel);
+};

+ 0 - 50
public/app/plugins/panel/gauge/GaugeOptionsBox.tsx

@@ -1,50 +0,0 @@
-// Libraries
-import React, { PureComponent } from 'react';
-
-// Components
-import { Switch, PanelOptionsGroup } from '@grafana/ui';
-
-// Types
-import { FormField, PanelEditorProps } from '@grafana/ui';
-import { GaugeOptions } from './types';
-
-export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
-  labelWidth = 8;
-
-  onToggleThresholdLabels = () =>
-    this.props.onOptionsChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
-
-  onToggleThresholdMarkers = () =>
-    this.props.onOptionsChange({
-      ...this.props.options,
-      showThresholdMarkers: !this.props.options.showThresholdMarkers,
-    });
-
-  onMinValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, minValue: target.value });
-
-  onMaxValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, maxValue: target.value });
-
-  render() {
-    const { options } = this.props;
-    const { maxValue, minValue, showThresholdLabels, showThresholdMarkers } = options;
-
-    return (
-      <PanelOptionsGroup title="Gauge">
-        <FormField label="Min value" labelWidth={this.labelWidth} onChange={this.onMinValueChange} value={minValue} />
-        <FormField label="Max value" labelWidth={this.labelWidth} onChange={this.onMaxValueChange} value={maxValue} />
-        <Switch
-          label="Show labels"
-          labelClass={`width-${this.labelWidth}`}
-          checked={showThresholdLabels}
-          onChange={this.onToggleThresholdLabels}
-        />
-        <Switch
-          label="Show markers"
-          labelClass={`width-${this.labelWidth}`}
-          checked={showThresholdMarkers}
-          onChange={this.onToggleThresholdMarkers}
-        />
-      </PanelOptionsGroup>
-    );
-  }
-}

+ 12 - 10
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -5,35 +5,37 @@ import React, { PureComponent } from 'react';
 import { config } from 'app/core/config';
 
 // Components
-import { Gauge } from '@grafana/ui';
+import { Gauge, FieldDisplay, getFieldDisplayValues } from '@grafana/ui';
 
 // Types
 import { GaugeOptions } from './types';
-import { DisplayValue, PanelProps, getSingleStatDisplayValues, VizRepeater } from '@grafana/ui';
+import { PanelProps, VizRepeater } from '@grafana/ui';
 
 export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
-  renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
+  renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
     const { options } = this.props;
+    const { fieldOptions } = options;
+    const { field, display } = value;
 
     return (
       <Gauge
-        value={value}
+        value={display}
         width={width}
         height={height}
-        thresholds={options.thresholds}
+        thresholds={fieldOptions.thresholds}
         showThresholdLabels={options.showThresholdLabels}
         showThresholdMarkers={options.showThresholdMarkers}
-        minValue={options.minValue}
-        maxValue={options.maxValue}
+        minValue={field.min}
+        maxValue={field.max}
         theme={config.theme}
       />
     );
   };
 
-  getValues = (): DisplayValue[] => {
+  getValues = (): FieldDisplay[] => {
     const { data, options, replaceVariables } = this.props;
-    return getSingleStatDisplayValues({
-      ...options,
+    return getFieldDisplayValues({
+      fieldOptions: options.fieldOptions,
       replaceVariables,
       theme: config.theme,
       data: data.series,

+ 60 - 15
public/app/plugins/panel/gauge/GaugePanelEditor.tsx

@@ -7,44 +7,89 @@ import {
   PanelOptionsGrid,
   ValueMappingsEditor,
   ValueMapping,
-  SingleStatValueOptions,
-  SingleStatValueEditor,
+  FieldDisplayOptions,
+  FieldDisplayEditor,
+  Field,
+  FieldPropertiesEditor,
+  Switch,
 } from '@grafana/ui';
 
-import { GaugeOptionsBox } from './GaugeOptionsBox';
 import { GaugeOptions } from './types';
 
 export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
-  onThresholdsChanged = (thresholds: Threshold[]) =>
+  labelWidth = 6;
+
+  onToggleThresholdLabels = () =>
+    this.props.onOptionsChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
+
+  onToggleThresholdMarkers = () =>
     this.props.onOptionsChange({
       ...this.props.options,
+      showThresholdMarkers: !this.props.options.showThresholdMarkers,
+    });
+
+  onThresholdsChanged = (thresholds: Threshold[]) =>
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
       thresholds,
     });
 
-  onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
-    this.props.onOptionsChange({
-      ...this.props.options,
-      valueMappings,
+  onValueMappingsChanged = (mappings: ValueMapping[]) =>
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
+      mappings,
     });
 
-  onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
+  onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
     this.props.onOptionsChange({
       ...this.props.options,
-      valueOptions,
+      fieldOptions,
+    });
+
+  onDefaultsChange = (field: Partial<Field>) => {
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
+      defaults: field,
     });
+  };
 
   render() {
-    const { onOptionsChange, options } = this.props;
+    const { options } = this.props;
+    const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
 
     return (
       <>
         <PanelOptionsGrid>
-          <SingleStatValueEditor onChange={this.onValueOptionsChanged} value={options.valueOptions} />
-          <GaugeOptionsBox onOptionsChange={onOptionsChange} options={options} />
-          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
+          <FieldDisplayEditor
+            onChange={this.onDisplayOptionsChanged}
+            options={fieldOptions}
+            labelWidth={this.labelWidth}
+          >
+            <Switch
+              label="Labels"
+              labelClass={`width-${this.labelWidth}`}
+              checked={showThresholdLabels}
+              onChange={this.onToggleThresholdLabels}
+            />
+            <Switch
+              label="Markers"
+              labelClass={`width-${this.labelWidth}`}
+              checked={showThresholdMarkers}
+              onChange={this.onToggleThresholdMarkers}
+            />
+          </FieldDisplayEditor>
+
+          <FieldPropertiesEditor
+            title="Field"
+            showMinMax={true}
+            onChange={this.onDefaultsChange}
+            options={fieldOptions.defaults}
+          />
+
+          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
         </PanelOptionsGrid>
 
-        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
       </>
     );
   }

+ 53 - 0
public/app/plugins/panel/gauge/__snapshots__/GaugeMigrations.test.ts.snap

@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Gauge Panel Migrations from 6.1.1 1`] = `
+Object {
+  "fieldOptions": Object {
+    "calcs": Array [
+      "last",
+    ],
+    "defaults": Object {
+      "decimals": 3,
+      "max": "50",
+      "min": "-50",
+      "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",
+  "showThresholdLabels": true,
+  "showThresholdMarkers": true,
+}
+`;

+ 12 - 14
public/app/plugins/panel/gauge/types.ts

@@ -1,25 +1,23 @@
-import { VizOrientation, ReducerID, SingleStatBaseOptions } from '@grafana/ui';
+import { VizOrientation, FieldDisplayOptions } from '@grafana/ui';
+import { SingleStatBaseOptions } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
+import { standardFieldDisplayOptions } from '../singlestat2/types';
 
 export interface GaugeOptions extends SingleStatBaseOptions {
-  maxValue: number;
-  minValue: number;
   showThresholdLabels: boolean;
   showThresholdMarkers: boolean;
 }
 
+export const standardGaugeFieldOptions: FieldDisplayOptions = {
+  ...standardFieldDisplayOptions,
+  defaults: {
+    min: 0,
+    max: 100,
+  },
+};
+
 export const defaults: GaugeOptions = {
-  minValue: 0,
-  maxValue: 100,
   showThresholdMarkers: true,
   showThresholdLabels: false,
-  valueOptions: {
-    prefix: '',
-    suffix: '',
-    decimals: null,
-    stat: ReducerID.mean,
-    unit: 'none',
-  },
-  valueMappings: [],
-  thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+  fieldOptions: standardGaugeFieldOptions,
   orientation: VizOrientation.Auto,
 };

+ 3 - 1
public/app/plugins/panel/graph2/getGraphSeriesModel.ts

@@ -23,7 +23,9 @@ export const getGraphSeriesModel = (
   const graphs: GraphSeriesXY[] = [];
 
   const displayProcessor = getDisplayProcessor({
-    decimals: legendOptions.decimals,
+    field: {
+      decimals: legendOptions.decimals,
+    },
   });
 
   for (const series of data.series) {

+ 4 - 6
public/app/plugins/panel/piechart/PieChartPanel.tsx

@@ -5,7 +5,7 @@ import React, { PureComponent } from 'react';
 import { config } from 'app/core/config';
 
 // Components
-import { PieChart, getSingleStatDisplayValues } from '@grafana/ui';
+import { PieChart, getFieldDisplayValues } from '@grafana/ui';
 
 // Types
 import { PieChartOptions } from './types';
@@ -17,14 +17,12 @@ export class PieChartPanel extends PureComponent<Props> {
   render() {
     const { width, height, options, data, replaceVariables } = this.props;
 
-    const values = getSingleStatDisplayValues({
-      valueMappings: options.valueMappings,
-      thresholds: options.thresholds,
-      valueOptions: options.valueOptions,
+    const values = getFieldDisplayValues({
+      fieldOptions: options.fieldOptions,
       data: data.series,
       theme: config.theme,
       replaceVariables: replaceVariables,
-    });
+    }).map(v => v.display);
 
     return (
       <PieChart

+ 28 - 10
public/app/plugins/panel/piechart/PieChartPanelEditor.tsx

@@ -4,37 +4,55 @@ import {
   PanelOptionsGrid,
   ValueMappingsEditor,
   ValueMapping,
-  SingleStatValueOptions,
-  SingleStatValueEditor,
+  FieldDisplayEditor,
+  FieldDisplayOptions,
+  FieldPropertiesEditor,
+  Field,
 } from '@grafana/ui';
 
 import { PieChartOptionsBox } from './PieChartOptionsBox';
 import { PieChartOptions } from './types';
 
 export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
-  onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
-    this.props.onOptionsChange({
-      ...this.props.options,
-      valueMappings,
+  onValueMappingsChanged = (mappings: ValueMapping[]) =>
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
+      mappings,
     });
 
-  onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
+  onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
     this.props.onOptionsChange({
       ...this.props.options,
-      valueOptions,
+      fieldOptions,
+    });
+
+  onDefaultsChange = (field: Partial<Field>) => {
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
+      defaults: field,
     });
+  };
 
   render() {
     const { onOptionsChange, options } = this.props;
+    const { fieldOptions } = options;
 
     return (
       <>
         <PanelOptionsGrid>
-          <SingleStatValueEditor onChange={this.onValueOptionsChanged} value={options.valueOptions} />
+          <FieldDisplayEditor onChange={this.onDisplayOptionsChanged} options={fieldOptions} />
+
+          <FieldPropertiesEditor
+            title="Field (default)"
+            showMinMax={true}
+            onChange={this.onDefaultsChange}
+            options={fieldOptions.defaults}
+          />
+
           <PieChartOptionsBox onOptionsChange={onOptionsChange} options={options} />
         </PanelOptionsGrid>
 
-        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
       </>
     );
   }

+ 8 - 8
public/app/plugins/panel/piechart/types.ts

@@ -1,4 +1,5 @@
 import { PieChartType, ReducerID, VizOrientation, SingleStatBaseOptions } from '@grafana/ui';
+import { standardFieldDisplayOptions } from '../singlestat2/types';
 
 export interface PieChartOptions extends SingleStatBaseOptions {
   pieType: PieChartType;
@@ -8,13 +9,12 @@ export interface PieChartOptions extends SingleStatBaseOptions {
 export const defaults: PieChartOptions = {
   pieType: PieChartType.PIE,
   strokeWidth: 1,
-  valueOptions: {
-    unit: 'short',
-    stat: ReducerID.last,
-    suffix: '',
-    prefix: '',
-  },
-  valueMappings: [],
-  thresholds: [],
   orientation: VizOrientation.Auto,
+  fieldOptions: {
+    ...standardFieldDisplayOptions,
+    calcs: [ReducerID.last],
+    defaults: {
+      unit: 'short',
+    },
+  },
 };

+ 31 - 13
public/app/plugins/panel/singlestat2/SingleStatEditor.tsx

@@ -7,8 +7,10 @@ import {
   PanelOptionsGrid,
   ValueMappingsEditor,
   ValueMapping,
-  SingleStatValueOptions,
-  SingleStatValueEditor,
+  FieldDisplayOptions,
+  FieldDisplayEditor,
+  FieldPropertiesEditor,
+  Field,
 } from '@grafana/ui';
 
 import { SingleStatOptions, SparklineOptions } from './types';
@@ -18,21 +20,21 @@ import { SparklineEditor } from './SparklineEditor';
 
 export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
   onThresholdsChanged = (thresholds: Threshold[]) =>
-    this.props.onOptionsChange({
-      ...this.props.options,
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
       thresholds,
     });
 
-  onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
-    this.props.onOptionsChange({
-      ...this.props.options,
-      valueMappings,
+  onValueMappingsChanged = (mappings: ValueMapping[]) =>
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
+      mappings,
     });
 
-  onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
+  onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
     this.props.onOptionsChange({
       ...this.props.options,
-      valueOptions,
+      fieldOptions,
     });
 
   onSparklineChanged = (sparkline: SparklineOptions) =>
@@ -41,21 +43,37 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
       sparkline,
     });
 
+  onDefaultsChange = (field: Partial<Field>) => {
+    this.onDisplayOptionsChanged({
+      ...this.props.options.fieldOptions,
+      override: field,
+    });
+  };
+
   render() {
     const { options } = this.props;
+    const { fieldOptions } = options;
 
     return (
       <>
         <PanelOptionsGrid>
-          <SingleStatValueEditor onChange={this.onValueOptionsChanged} value={options.valueOptions} />
+          <FieldDisplayEditor onChange={this.onDisplayOptionsChanged} options={fieldOptions} />
+
+          <FieldPropertiesEditor
+            title="Field (default)"
+            showMinMax={true}
+            onChange={this.onDefaultsChange}
+            options={fieldOptions.defaults}
+          />
+
           <FontSizeEditor options={options} onChange={this.props.onOptionsChange} />
           <ColoringEditor options={options} onChange={this.props.onOptionsChange} />
           <SparklineEditor options={options.sparkline} onChange={this.onSparklineChanged} />
 
-          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
+          <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={fieldOptions.thresholds} />
         </PanelOptionsGrid>
 
-        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={fieldOptions.mappings} />
       </>
     );
   }

+ 23 - 119
public/app/plugins/panel/singlestat2/SingleStatPanel.tsx

@@ -3,134 +3,38 @@ import React, { PureComponent } from 'react';
 
 // Utils & Services
 import { config } from 'app/core/config';
-import { getFlotPairs } from '@grafana/ui/src/utils/flotPairs';
-
-// Components
-import { VizRepeater } from '@grafana/ui/src/components';
-import { BigValueSparkline, BigValue } from '@grafana/ui/src/components/BigValue/BigValue';
 
 // Types
 import { SingleStatOptions } from './types';
-import {
-  DisplayValue,
-  PanelProps,
-  getDisplayProcessor,
-  NullValueMode,
-  reduceField,
-  FieldCache,
-  FieldType,
-} from '@grafana/ui';
-
-interface SingleStatDisplay {
-  value: DisplayValue;
-  prefix?: DisplayValue;
-  suffix?: DisplayValue;
-  sparkline?: BigValueSparkline;
-  backgroundColor?: string;
-}
+import { PanelProps, getFieldDisplayValues, VizRepeater, FieldDisplay, BigValue } from '@grafana/ui';
+import { BigValueSparkline } from '@grafana/ui/src/components/BigValue/BigValue';
 
 export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
-  renderValue = (value: SingleStatDisplay, width: number, height: number): JSX.Element => {
-    return <BigValue {...value} width={width} height={height} theme={config.theme} />;
-  };
+  renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
+    let sparkline: BigValueSparkline;
+    if (value.sparkline) {
+      const { timeRange, options } = this.props;
+
+      sparkline = {
+        ...options.sparkline,
+        data: value.sparkline,
+        minX: timeRange.from.valueOf(),
+        maxX: timeRange.to.valueOf(),
+      };
+    }
 
-  getValues = (): SingleStatDisplay[] => {
-    const { data, replaceVariables, options, timeRange } = this.props;
-    const { valueOptions, valueMappings } = options;
+    return <BigValue value={value.display} sparkline={sparkline} width={width} height={height} theme={config.theme} />;
+  };
 
-    const display = getDisplayProcessor({
-      unit: valueOptions.unit,
-      decimals: valueOptions.decimals,
-      mappings: valueMappings,
-      thresholds: options.thresholds,
+  getValues = (): FieldDisplay[] => {
+    const { data, options, replaceVariables } = this.props;
+    return getFieldDisplayValues({
+      ...options,
+      replaceVariables,
       theme: config.theme,
+      data: data.series,
+      sparkline: options.sparkline.show,
     });
-
-    const { colorBackground, colorValue, colorPrefix, colorPostfix, sparkline } = options;
-    const { stat } = valueOptions;
-
-    const values: SingleStatDisplay[] = [];
-    for (const series of data.series) {
-      const fieldCache = new FieldCache(series.fields);
-      const timeColumn = sparkline.show ? fieldCache.getFirstFieldOfType(FieldType.time) : null;
-      const numberFields = fieldCache.getFields(FieldType.number);
-
-      for (let i = 0; i < numberFields.length; i++) {
-        const field = numberFields[i];
-        const stats = reduceField({
-          series,
-          fieldIndex: field.index,
-          reducers: [stat], // The stats to calculate
-          nullValueMode: NullValueMode.Null,
-        });
-
-        const v: SingleStatDisplay = {
-          value: display(stats[stat]),
-        };
-        v.value.title = replaceVariables(field.name);
-
-        const color = v.value.color;
-        if (!colorValue) {
-          delete v.value.color;
-        }
-
-        if (colorBackground) {
-          v.backgroundColor = color;
-        }
-
-        if (options.valueFontSize) {
-          v.value.fontSize = options.valueFontSize;
-        }
-
-        if (valueOptions.prefix) {
-          v.prefix = {
-            text: replaceVariables(valueOptions.prefix),
-            numeric: NaN,
-            color: colorPrefix ? color : null,
-            fontSize: options.prefixFontSize,
-          };
-        }
-        if (valueOptions.suffix) {
-          v.suffix = {
-            text: replaceVariables(valueOptions.suffix),
-            numeric: NaN,
-            color: colorPostfix ? color : null,
-            fontSize: options.postfixFontSize,
-          };
-        }
-
-        if (sparkline.show && timeColumn) {
-          const points = getFlotPairs({
-            series,
-            xIndex: timeColumn.index,
-            yIndex: field.index,
-            nullValueMode: NullValueMode.Null,
-          });
-
-          v.sparkline = {
-            ...sparkline,
-            data: points,
-            minX: timeRange.from.valueOf(),
-            maxX: timeRange.to.valueOf(),
-          };
-        }
-
-        values.push(v);
-      }
-    }
-
-    if (values.length === 0) {
-      values.push({
-        value: {
-          numeric: 0,
-          text: 'No data',
-        },
-      });
-    } else if (values.length === 1) {
-      // Don't show title for single item
-      values[0].value.title = null;
-    }
-    return values;
   };
 
   render() {

+ 11 - 11
public/app/plugins/panel/singlestat2/types.ts

@@ -1,4 +1,4 @@
-import { VizOrientation, ReducerID, SingleStatBaseOptions } from '@grafana/ui';
+import { VizOrientation, ReducerID, SingleStatBaseOptions, FieldDisplayOptions } from '@grafana/ui';
 
 export interface SparklineOptions {
   show: boolean;
@@ -21,6 +21,15 @@ export interface SingleStatOptions extends SingleStatBaseOptions {
   sparkline: SparklineOptions;
 }
 
+export const standardFieldDisplayOptions: FieldDisplayOptions = {
+  values: false,
+  calcs: [ReducerID.mean],
+  defaults: {},
+  override: {},
+  mappings: [],
+  thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+};
+
 export const defaults: SingleStatOptions = {
   sparkline: {
     show: true,
@@ -28,15 +37,6 @@ export const defaults: SingleStatOptions = {
     lineColor: 'rgb(31, 120, 193)',
     fillColor: 'rgba(31, 118, 189, 0.18)',
   },
-
-  valueOptions: {
-    prefix: '',
-    suffix: '',
-    decimals: null,
-    stat: ReducerID.mean,
-    unit: 'none',
-  },
-  valueMappings: [],
-  thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+  fieldOptions: standardFieldDisplayOptions,
   orientation: VizOrientation.Auto,
 };