Просмотр исходного кода

Registry: add a reusable function registry (#17047)

Ryan McKinley 6 лет назад
Родитель
Сommit
c194ae1ba5
35 измененных файлов с 406 добавлено и 283 удалено
  1. 1 0
      packages/grafana-data/src/types/index.ts
  2. 10 0
      packages/grafana-data/src/types/select.ts
  3. 43 20
      packages/grafana-data/src/utils/fieldReducer.test.ts
  4. 111 146
      packages/grafana-data/src/utils/fieldReducer.ts
  5. 1 0
      packages/grafana-data/src/utils/index.ts
  6. 134 0
      packages/grafana-data/src/utils/registry.ts
  7. 3 3
      packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx
  8. 4 4
      packages/grafana-ui/src/components/Select/ButtonSelect.story.tsx
  9. 6 5
      packages/grafana-ui/src/components/Select/ButtonSelect.tsx
  10. 7 14
      packages/grafana-ui/src/components/Select/Select.tsx
  11. 2 3
      packages/grafana-ui/src/components/SetInterval/SetInterval.tsx
  12. 4 4
      packages/grafana-ui/src/components/SingleStatShared/FieldDisplayEditor.tsx
  13. 2 3
      packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx
  14. 5 2
      packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts
  15. 6 15
      packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx
  16. 4 5
      packages/grafana-ui/src/components/TimePicker/TimePicker.tsx
  17. 1 1
      packages/grafana-ui/src/components/index.ts
  18. 3 2
      public/app/core/components/PermissionList/AddPermission.tsx
  19. 3 2
      public/app/core/components/PermissionList/PermissionListItem.tsx
  20. 3 2
      public/app/core/components/Select/DataSourcePicker.tsx
  21. 4 3
      public/app/core/components/Select/MetricSelect.tsx
  22. 3 2
      public/app/features/explore/AdHocFilter.tsx
  23. 5 5
      public/app/features/explore/ExploreToolbar.tsx
  24. 2 2
      public/app/features/teams/TeamMemberRow.test.tsx
  25. 3 2
      public/app/features/teams/TeamMemberRow.tsx
  26. 3 3
      public/app/plugins/datasource/input/InputQueryEditor.tsx
  27. 2 5
      public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx
  28. 8 9
      public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx
  29. 2 2
      public/app/plugins/datasource/stackdriver/components/Alignments.tsx
  30. 2 3
      public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx
  31. 3 2
      public/app/plugins/datasource/testdata/QueryEditor.tsx
  32. 4 3
      public/app/plugins/panel/bargauge/types.ts
  33. 3 4
      public/app/plugins/panel/gauge/GaugeMigrations.ts
  34. 5 4
      public/app/plugins/panel/singlestat2/FontSizeEditor.tsx
  35. 4 3
      public/app/plugins/panel/text2/TextPanelEditor.tsx

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

@@ -2,6 +2,7 @@ export * from './data';
 export * from './dataLink';
 export * from './logs';
 export * from './navModel';
+export * from './select';
 export * from './time';
 export * from './threshold';
 export * from './utils';

+ 10 - 0
packages/grafana-data/src/types/select.ts

@@ -0,0 +1,10 @@
+/**
+ * Used in select elements
+ */
+export interface SelectableValue<T = any> {
+  label?: string;
+  value?: T;
+  imgUrl?: string;
+  description?: string;
+  [key: string]: any;
+}

+ 43 - 20
packages/grafana-data/src/utils/fieldReducer.test.ts

@@ -1,6 +1,14 @@
-import { getFieldReducers, ReducerID, reduceField } from './index';
+import { fieldReducers, ReducerID, reduceField } from './fieldReducer';
 
 import _ from 'lodash';
+import { DataFrame } from '../types/data';
+
+/**
+ * Run a reducer and get back the value
+ */
+function reduce(series: DataFrame, fieldIndex: number, id: string): any {
+  return reduceField({ series, fieldIndex, reducers: [id] })[id];
+}
 
 describe('Stats Calculators', () => {
   const basicTable = {
@@ -9,29 +17,16 @@ describe('Stats Calculators', () => {
   };
 
   it('should load all standard stats', () => {
-    const names = [
-      ReducerID.sum,
-      ReducerID.max,
-      ReducerID.min,
-      ReducerID.logmin,
-      ReducerID.mean,
-      ReducerID.last,
-      ReducerID.first,
-      ReducerID.count,
-      ReducerID.range,
-      ReducerID.diff,
-      ReducerID.step,
-      ReducerID.delta,
-      // ReducerID.allIsZero,
-      // ReducerID.allIsNull,
-    ];
-    const stats = getFieldReducers(names);
-    expect(stats.length).toBe(names.length);
+    for (const id of Object.keys(ReducerID)) {
+      const reducer = fieldReducers.getIfExists(id);
+      const found = reducer ? reducer.id : '<NOT FOUND>';
+      expect(found).toEqual(id);
+    }
   });
 
   it('should fail to load unknown stats', () => {
     const names = ['not a stat', ReducerID.max, ReducerID.min, 'also not a stat'];
-    const stats = getFieldReducers(names);
+    const stats = fieldReducers.list(names);
     expect(stats.length).toBe(2);
 
     const found = stats.map(v => v.id);
@@ -92,6 +87,34 @@ describe('Stats Calculators', () => {
     expect(stats.delta).toEqual(300);
   });
 
+  it('consistenly check allIsNull/allIsZero', () => {
+    const empty = {
+      fields: [{ name: 'A' }],
+      rows: [],
+    };
+    const allNull = ({
+      fields: [{ name: 'A' }],
+      rows: [null, null, null, null],
+    } as unknown) as DataFrame;
+    const allNull2 = {
+      fields: [{ name: 'A' }],
+      rows: [[null], [null], [null], [null]],
+    };
+    const allZero = {
+      fields: [{ name: 'A' }],
+      rows: [[0], [0], [0], [0]],
+    };
+
+    expect(reduce(empty, 0, ReducerID.allIsNull)).toEqual(true);
+    expect(reduce(allNull, 0, ReducerID.allIsNull)).toEqual(true);
+    expect(reduce(allNull2, 0, ReducerID.allIsNull)).toEqual(true);
+
+    expect(reduce(empty, 0, ReducerID.allIsZero)).toEqual(false);
+    expect(reduce(allNull, 0, ReducerID.allIsZero)).toEqual(false);
+    expect(reduce(allNull2, 0, ReducerID.allIsZero)).toEqual(false);
+    expect(reduce(allZero, 0, ReducerID.allIsZero)).toEqual(true);
+  });
+
   it('consistent results for first/last value with null', () => {
     const info = [
       {

+ 111 - 146
packages/grafana-data/src/utils/fieldReducer.ts

@@ -1,7 +1,8 @@
 // Libraries
 import isNumber from 'lodash/isNumber';
 
-import { DataFrame, NullValueMode } from '../types/index';
+import { DataFrame, NullValueMode } from '../types';
+import { Registry, RegistryItem } from './registry';
 
 export enum ReducerID {
   sum = 'sum',
@@ -34,38 +35,13 @@ export interface FieldCalcs {
 // Internal function
 type FieldReducer = (data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
 
-export interface FieldReducerInfo {
-  id: string;
-  name: string;
-  description: string;
-  alias?: string; // optional secondary key.  'avg' vs 'mean', 'total' vs 'sum'
-
+export interface FieldReducerInfo extends RegistryItem {
   // Internal details
   emptyInputResult?: any; // typically null, but some things like 'count' & 'sum' should be zero
   standard: boolean; // The most common stats can all be calculated in a single pass
   reduce?: FieldReducer;
 }
 
-/**
- * @param ids list of stat names or null to get all of them
- */
-export function getFieldReducers(ids?: string[]): FieldReducerInfo[] {
-  if (ids === null || ids === undefined) {
-    if (!hasBuiltIndex) {
-      getById(ReducerID.mean);
-    }
-    return listOfStats;
-  }
-
-  return ids.reduce((list, id) => {
-    const stat = getById(id);
-    if (stat) {
-      list.push(stat);
-    }
-    return list;
-  }, new Array<FieldReducerInfo>());
-}
-
 interface ReduceFieldOptions {
   series: DataFrame;
   fieldIndex: number;
@@ -83,7 +59,7 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
     return {};
   }
 
-  const queue = getFieldReducers(reducers);
+  const queue = fieldReducers.list(reducers);
 
   // Return early for empty series
   // This lets the concrete implementations assume at least one row
@@ -122,122 +98,107 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
 //
 // ------------------------------------------------------------------------------
 
-// private registry of all stats
-interface TableStatIndex {
-  [id: string]: FieldReducerInfo;
-}
-
-const listOfStats: FieldReducerInfo[] = [];
-const index: TableStatIndex = {};
-let hasBuiltIndex = false;
-
-function getById(id: string): FieldReducerInfo | undefined {
-  if (!hasBuiltIndex) {
-    [
-      {
-        id: ReducerID.lastNotNull,
-        name: 'Last (not null)',
-        description: 'Last non-null value',
-        standard: true,
-        alias: 'current',
-        reduce: calculateLastNotNull,
-      },
-      {
-        id: ReducerID.last,
-        name: 'Last',
-        description: 'Last Value',
-        standard: true,
-        reduce: calculateLast,
-      },
-      { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst },
-      {
-        id: ReducerID.firstNotNull,
-        name: 'First (not null)',
-        description: 'First non-null value',
-        standard: true,
-        reduce: calculateFirstNotNull,
-      },
-      { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true },
-      { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true },
-      { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
-      {
-        id: ReducerID.sum,
-        name: 'Total',
-        description: 'The sum of all values',
-        emptyInputResult: 0,
-        standard: true,
-        alias: 'total',
-      },
-      {
-        id: ReducerID.count,
-        name: 'Count',
-        description: 'Number of values in response',
-        emptyInputResult: 0,
-        standard: true,
-      },
-      {
-        id: ReducerID.range,
-        name: 'Range',
-        description: 'Difference between minimum and maximum values',
-        standard: true,
-      },
-      {
-        id: ReducerID.delta,
-        name: 'Delta',
-        description: 'Cumulative change in value',
-        standard: true,
-      },
-      {
-        id: ReducerID.step,
-        name: 'Step',
-        description: 'Minimum interval between values',
-        standard: true,
-      },
-      {
-        id: ReducerID.diff,
-        name: 'Difference',
-        description: 'Difference between first and last values',
-        standard: true,
-      },
-      {
-        id: ReducerID.logmin,
-        name: 'Min (above zero)',
-        description: 'Used for log min scale',
-        standard: true,
-      },
-      {
-        id: ReducerID.changeCount,
-        name: 'Change Count',
-        description: 'Number of times the value changes',
-        standard: false,
-        reduce: calculateChangeCount,
-      },
-      {
-        id: ReducerID.distinctCount,
-        name: 'Distinct Count',
-        description: 'Number of distinct values',
-        standard: false,
-        reduce: calculateDistinctCount,
-      },
-    ].forEach(info => {
-      const { id, alias } = info;
-      if (index.hasOwnProperty(id)) {
-        console.warn('Duplicate Stat', id, info, index);
-      }
-      index[id] = info;
-      if (alias) {
-        if (index.hasOwnProperty(alias)) {
-          console.warn('Duplicate Stat (alias)', alias, info, index);
-        }
-        index[alias] = info;
-      }
-      listOfStats.push(info);
-    });
-    hasBuiltIndex = true;
-  }
-
-  return index[id];
-}
+export const fieldReducers = new Registry<FieldReducerInfo>(() => [
+  {
+    id: ReducerID.lastNotNull,
+    name: 'Last (not null)',
+    description: 'Last non-null value',
+    standard: true,
+    alias: 'current',
+    reduce: calculateLastNotNull,
+  },
+  {
+    id: ReducerID.last,
+    name: 'Last',
+    description: 'Last Value',
+    standard: true,
+    reduce: calculateLast,
+  },
+  { id: ReducerID.first, name: 'First', description: 'First Value', standard: true, reduce: calculateFirst },
+  {
+    id: ReducerID.firstNotNull,
+    name: 'First (not null)',
+    description: 'First non-null value',
+    standard: true,
+    reduce: calculateFirstNotNull,
+  },
+  { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true },
+  { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true },
+  { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
+  {
+    id: ReducerID.sum,
+    name: 'Total',
+    description: 'The sum of all values',
+    emptyInputResult: 0,
+    standard: true,
+    alias: 'total',
+  },
+  {
+    id: ReducerID.count,
+    name: 'Count',
+    description: 'Number of values in response',
+    emptyInputResult: 0,
+    standard: true,
+  },
+  {
+    id: ReducerID.range,
+    name: 'Range',
+    description: 'Difference between minimum and maximum values',
+    standard: true,
+  },
+  {
+    id: ReducerID.delta,
+    name: 'Delta',
+    description: 'Cumulative change in value',
+    standard: true,
+  },
+  {
+    id: ReducerID.step,
+    name: 'Step',
+    description: 'Minimum interval between values',
+    standard: true,
+  },
+  {
+    id: ReducerID.diff,
+    name: 'Difference',
+    description: 'Difference between first and last values',
+    standard: true,
+  },
+  {
+    id: ReducerID.logmin,
+    name: 'Min (above zero)',
+    description: 'Used for log min scale',
+    standard: true,
+  },
+  {
+    id: ReducerID.allIsZero,
+    name: 'All Zeros',
+    description: 'All values are zero',
+    emptyInputResult: false,
+    standard: true,
+  },
+  {
+    id: ReducerID.allIsNull,
+    name: 'All Nulls',
+    description: 'All values are null',
+    emptyInputResult: true,
+    standard: true,
+  },
+  {
+    id: ReducerID.changeCount,
+    name: 'Change Count',
+    description: 'Number of times the value changes',
+    standard: false,
+    reduce: calculateChangeCount,
+  },
+  {
+    id: ReducerID.distinctCount,
+    name: 'Distinct Count',
+    description: 'Number of distinct values',
+    standard: false,
+    reduce: calculateDistinctCount,
+  },
+]);
 
 function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
   const calcs = {
@@ -253,7 +214,7 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
     count: 0,
     nonNullCount: 0,
     allIsNull: true,
-    allIsZero: false,
+    allIsZero: true,
     range: null,
     diff: null,
     delta: 0,
@@ -264,7 +225,7 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
   } as FieldCalcs;
 
   for (let i = 0; i < data.rows.length; i++) {
-    let currentValue = data.rows[i][fieldIndex];
+    let currentValue = data.rows[i] ? data.rows[i][fieldIndex] : null;
     if (i === 0) {
       calcs.first = currentValue;
     }
@@ -350,6 +311,10 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
     calcs.mean = calcs.sum! / calcs.nonNullCount;
   }
 
+  if (calcs.allIsNull) {
+    calcs.allIsZero = false;
+  }
+
   if (calcs.max !== null && calcs.min !== null) {
     calcs.range = calcs.max - calcs.min;
   }

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

@@ -1,4 +1,5 @@
 export * from './string';
+export * from './registry';
 export * from './markdown';
 export * from './processDataFrame';
 export * from './csv';

+ 134 - 0
packages/grafana-data/src/utils/registry.ts

@@ -0,0 +1,134 @@
+import { SelectableValue } from '../types/select';
+
+export interface RegistryItem {
+  id: string; // Unique Key -- saved in configs
+  name: string; // Display Name, can change without breaking configs
+  description: string;
+  aliasIds?: string[]; // when the ID changes, we may want backwards compatibility ('current' => 'last')
+
+  /**
+   * Some extensions should not be user selectable
+   *  like: 'all' and 'any' matchers;
+   */
+  excludeFromPicker?: boolean;
+}
+
+interface RegistrySelectInfo {
+  options: Array<SelectableValue<string>>;
+  current: Array<SelectableValue<string>>;
+}
+
+export class Registry<T extends RegistryItem> {
+  private ordered: T[] = [];
+  private byId = new Map<string, T>();
+  private initalized = false;
+
+  constructor(private init?: () => T[]) {}
+
+  getIfExists(id: string | undefined): T | undefined {
+    if (!this.initalized) {
+      if (this.init) {
+        for (const ext of this.init()) {
+          this.register(ext);
+        }
+      }
+      this.sort();
+      this.initalized = true;
+    }
+    if (id) {
+      return this.byId.get(id);
+    }
+    return undefined;
+  }
+
+  get(id: string): T {
+    const v = this.getIfExists(id);
+    if (!v) {
+      throw new Error('Undefined: ' + id);
+    }
+    return v;
+  }
+
+  selectOptions(current?: string[], filter?: (ext: T) => boolean): RegistrySelectInfo {
+    if (!this.initalized) {
+      this.getIfExists('xxx'); // will trigger init
+    }
+
+    const select = {
+      options: [],
+      current: [],
+    } as RegistrySelectInfo;
+
+    const currentIds: any = {};
+    if (current) {
+      for (const id of current) {
+        currentIds[id] = true;
+      }
+    }
+
+    for (const ext of this.ordered) {
+      if (ext.excludeFromPicker) {
+        continue;
+      }
+      if (filter && !filter(ext)) {
+        continue;
+      }
+
+      const option = {
+        value: ext.id,
+        label: ext.name,
+        description: ext.description,
+      };
+
+      select.options.push(option);
+      if (currentIds[ext.id]) {
+        select.current.push(option);
+      }
+    }
+    return select;
+  }
+
+  /**
+   * Return a list of values by ID, or all values if not specified
+   */
+  list(ids?: any[]): T[] {
+    if (ids) {
+      const found: T[] = [];
+      for (const id of ids) {
+        const v = this.getIfExists(id);
+        if (v) {
+          found.push(v);
+        }
+      }
+      return found;
+    }
+    if (!this.initalized) {
+      this.getIfExists('xxx'); // will trigger init
+    }
+    return [...this.ordered]; // copy of everythign just in case
+  }
+
+  register(ext: T) {
+    if (this.byId.has(ext.id)) {
+      throw new Error('Duplicate Key:' + ext.id);
+    }
+    this.byId.set(ext.id, ext);
+    this.ordered.push(ext);
+
+    if (ext.aliasIds) {
+      for (const alias of ext.aliasIds) {
+        if (!this.byId.has(alias)) {
+          this.byId.set(alias, ext);
+        }
+      }
+    }
+
+    if (this.initalized) {
+      this.sort();
+    }
+  }
+
+  private sort() {
+    // TODO sort the list
+  }
+}

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

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import classNames from 'classnames';
-import { SelectOptionItem } from '../Select/Select';
+import { SelectableValue } from '@grafana/data';
 import { Tooltip } from '../Tooltip/Tooltip';
 import { ButtonSelect } from '../Select/ButtonSelect';
 
@@ -23,7 +23,7 @@ export class RefreshPicker extends PureComponent<Props> {
     super(props);
   }
 
-  intervalsToOptions = (intervals: string[] | undefined): Array<SelectOptionItem<string>> => {
+  intervalsToOptions = (intervals: string[] | undefined): Array<SelectableValue<string>> => {
     const intervalsOrDefault = intervals || defaultIntervals;
     const options = intervalsOrDefault
       .filter(str => str !== '')
@@ -37,7 +37,7 @@ export class RefreshPicker extends PureComponent<Props> {
     return options;
   };
 
-  onChangeSelect = (item: SelectOptionItem<string>) => {
+  onChangeSelect = (item: SelectableValue<string>) => {
     const { onIntervalChanged } = this.props;
     if (onIntervalChanged) {
       // @ts-ignore

+ 4 - 4
packages/grafana-ui/src/components/Select/ButtonSelect.story.tsx

@@ -4,7 +4,7 @@ import { action } from '@storybook/addon-actions';
 import { withKnobs, object, text } from '@storybook/addon-knobs';
 import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
 import { UseState } from '../../utils/storybook/UseState';
-import { SelectOptionItem } from './Select';
+import { SelectableValue } from '@grafana/data';
 import { ButtonSelect } from './ButtonSelect';
 
 const ButtonSelectStories = storiesOf('UI/Select/ButtonSelect', module);
@@ -12,9 +12,9 @@ const ButtonSelectStories = storiesOf('UI/Select/ButtonSelect', module);
 ButtonSelectStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
 
 ButtonSelectStories.add('default', () => {
-  const intialState: SelectOptionItem<string> = { label: 'A label', value: 'A value' };
-  const value = object<SelectOptionItem<string>>('Selected Value:', intialState);
-  const options = object<Array<SelectOptionItem<string>>>('Options:', [
+  const intialState: SelectableValue<string> = { label: 'A label', value: 'A value' };
+  const value = object<SelectableValue<string>>('Selected Value:', intialState);
+  const options = object<Array<SelectableValue<string>>>('Options:', [
     intialState,
     { label: 'Another label', value: 'Another value' },
   ]);

+ 6 - 5
packages/grafana-ui/src/components/Select/ButtonSelect.tsx

@@ -1,6 +1,7 @@
 import React, { PureComponent, ReactElement } from 'react';
-import Select, { SelectOptionItem } from './Select';
+import Select from './Select';
 import { PopperContent } from '../Tooltip/PopperController';
+import { SelectableValue } from '@grafana/data';
 
 interface ButtonComponentProps {
   label: ReactElement | string | undefined;
@@ -30,13 +31,13 @@ const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
 
 export interface Props<T> {
   className: string | undefined;
-  options: Array<SelectOptionItem<T>>;
-  value?: SelectOptionItem<T>;
+  options: Array<SelectableValue<T>>;
+  value?: SelectableValue<T>;
   label?: ReactElement | string;
   iconClass?: string;
   components?: any;
   maxMenuHeight?: number;
-  onChange: (item: SelectOptionItem<T>) => void;
+  onChange: (item: SelectableValue<T>) => void;
   tooltipContent?: PopperContent<any>;
   isMenuOpen?: boolean;
   onOpenMenu?: () => void;
@@ -45,7 +46,7 @@ export interface Props<T> {
 }
 
 export class ButtonSelect<T> extends PureComponent<Props<T>> {
-  onChange = (item: SelectOptionItem<T>) => {
+  onChange = (item: SelectableValue<T>) => {
     const { onChange } = this.props;
     onChange(item);
   };

+ 7 - 14
packages/grafana-ui/src/components/Select/Select.tsx

@@ -19,23 +19,16 @@ import resetSelectStyles from './resetSelectStyles';
 import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
 import { PopperContent } from '../Tooltip/PopperController';
 import { Tooltip } from '../Tooltip/Tooltip';
-
-export interface SelectOptionItem<T> {
-  label?: string;
-  value?: T;
-  imgUrl?: string;
-  description?: string;
-  [key: string]: any;
-}
+import { SelectableValue } from '@grafana/data';
 
 export interface CommonProps<T> {
   defaultValue?: any;
-  getOptionLabel?: (item: SelectOptionItem<T>) => string;
-  getOptionValue?: (item: SelectOptionItem<T>) => string;
-  onChange: (item: SelectOptionItem<T>) => {} | void;
+  getOptionLabel?: (item: SelectableValue<T>) => string;
+  getOptionValue?: (item: SelectableValue<T>) => string;
+  onChange: (item: SelectableValue<T>) => {} | void;
   placeholder?: string;
   width?: number;
-  value?: SelectOptionItem<T>;
+  value?: SelectableValue<T>;
   className?: string;
   isDisabled?: boolean;
   isSearchable?: boolean;
@@ -57,12 +50,12 @@ export interface CommonProps<T> {
 }
 
 export interface SelectProps<T> extends CommonProps<T> {
-  options: Array<SelectOptionItem<T>>;
+  options: Array<SelectableValue<T>>;
 }
 
 interface AsyncProps<T> extends CommonProps<T> {
   defaultOptions: boolean;
-  loadOptions: (query: string) => Promise<Array<SelectOptionItem<T>>>;
+  loadOptions: (query: string) => Promise<Array<SelectableValue<T>>>;
   loadingMessage?: () => string;
 }
 

+ 2 - 3
packages/grafana-ui/src/components/SetInterval/SetInterval.tsx

@@ -3,11 +3,10 @@ import { interval, Subscription, Subject, of, NEVER } from 'rxjs';
 import { tap, switchMap } from 'rxjs/operators';
 import _ from 'lodash';
 
-import { stringToMs } from '@grafana/data';
+import { stringToMs, SelectableValue } from '@grafana/data';
 import { isLive } from '../RefreshPicker/RefreshPicker';
-import { SelectOptionItem } from '../Select/Select';
 
-export function getIntervalFromString(strInterval: string): SelectOptionItem<number> {
+export function getIntervalFromString(strInterval: string): SelectableValue<number> {
   return {
     label: strInterval,
     value: stringToMs(strInterval),

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

@@ -8,10 +8,10 @@ import { StatsPicker } from '../StatsPicker/StatsPicker';
 
 // Types
 import { FieldDisplayOptions, DEFAULT_FIELD_DISPLAY_VALUES_LIMIT } from '../../utils/fieldDisplay';
-import Select, { SelectOptionItem } from '../Select/Select';
-import { Field, ReducerID, toNumberString, toIntegerOrUndefined } from '@grafana/data';
+import Select from '../Select/Select';
+import { Field, ReducerID, toNumberString, toIntegerOrUndefined, SelectableValue } from '@grafana/data';
 
-const showOptions: Array<SelectOptionItem<boolean>> = [
+const showOptions: Array<SelectableValue<boolean>> = [
   {
     value: true,
     label: 'All Values',
@@ -31,7 +31,7 @@ export interface Props {
 }
 
 export class FieldDisplayEditor extends PureComponent<Props> {
-  onShowValuesChange = (item: SelectOptionItem<boolean>) => {
+  onShowValuesChange = (item: SelectableValue<boolean>) => {
     const val = item.value === true;
     this.props.onChange({ ...this.props.value, values: val });
   };

+ 2 - 3
packages/grafana-ui/src/components/SingleStatShared/FieldPropertiesEditor.tsx

@@ -7,8 +7,7 @@ import { FormLabel } from '../FormLabel/FormLabel';
 import { UnitPicker } from '../UnitPicker/UnitPicker';
 
 // Types
-import { toIntegerOrUndefined, Field } from '@grafana/data';
-import { SelectOptionItem } from '../Select/Select';
+import { toIntegerOrUndefined, Field, SelectableValue } from '@grafana/data';
 
 import { VAR_SERIES_NAME, VAR_FIELD_NAME, VAR_CALC, VAR_CELL_PREFIX } from '../../utils/fieldDisplay';
 
@@ -54,7 +53,7 @@ export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMi
     [value.max, onChange]
   );
 
-  const onUnitChange = (unit: SelectOptionItem<string>) => {
+  const onUnitChange = (unit: SelectableValue<string>) => {
     onChange({ ...value, unit: unit.value });
   };
 

+ 5 - 2
packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts

@@ -3,7 +3,7 @@ import omit from 'lodash/omit';
 
 import { VizOrientation, PanelModel } from '../../types/panel';
 import { FieldDisplayOptions } from '../../utils/fieldDisplay';
-import { Field, getFieldReducers, Threshold, sortThresholds } from '@grafana/data';
+import { Field, fieldReducers, Threshold, sortThresholds } from '@grafana/data';
 
 export interface SingleStatBaseOptions {
   fieldOptions: FieldDisplayOptions;
@@ -48,7 +48,10 @@ export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseO
 
     // Make sure the stats have a valid name
     if (valueOptions.stat) {
-      fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
+      const reducer = fieldReducers.get(valueOptions.stat);
+      if (reducer) {
+        fieldOptions.calcs = [reducer.id];
+      }
     }
 
     field.min = old.minValue;

+ 6 - 15
packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx

@@ -5,8 +5,7 @@ import difference from 'lodash/difference';
 
 import { Select } from '../index';
 
-import { getFieldReducers } from '@grafana/data';
-import { SelectOptionItem } from '../Select/Select';
+import { fieldReducers, SelectableValue } from '@grafana/data';
 
 interface Props {
   placeholder?: string;
@@ -34,7 +33,7 @@ export class StatsPicker extends PureComponent<Props> {
   checkInput = () => {
     const { stats, allowMultiple, defaultStat, onChange } = this.props;
 
-    const current = getFieldReducers(stats);
+    const current = fieldReducers.list(stats);
     if (current.length !== stats.length) {
       const found = current.map(v => v.id);
       const notFound = difference(stats, found);
@@ -54,7 +53,7 @@ export class StatsPicker extends PureComponent<Props> {
     }
   };
 
-  onSelectionChange = (item: SelectOptionItem<string>) => {
+  onSelectionChange = (item: SelectableValue<string>) => {
     const { onChange } = this.props;
     if (isArray(item)) {
       onChange(item.map(v => v.value));
@@ -65,24 +64,16 @@ export class StatsPicker extends PureComponent<Props> {
 
   render() {
     const { width, stats, allowMultiple, defaultStat, placeholder } = this.props;
-    const options = getFieldReducers().map(s => {
-      return {
-        value: s.id,
-        label: s.name,
-        description: s.description,
-      };
-    });
-
-    const value: Array<SelectOptionItem<string>> = options.filter(option => stats.find(stat => option.value === stat));
 
+    const select = fieldReducers.selectOptions(stats);
     return (
       <Select
         width={width}
-        value={value}
+        value={select.current}
         isClearable={!defaultStat}
         isMulti={allowMultiple}
         isSearchable={true}
-        options={options}
+        options={select.options}
         placeholder={placeholder}
         onChange={this.onSelectionChange}
       />

+ 4 - 5
packages/grafana-ui/src/components/TimePicker/TimePicker.tsx

@@ -13,8 +13,7 @@ import { rangeUtil } from '@grafana/data';
 import { rawToTimeRange } from './time';
 
 // Types
-import { TimeRange, TimeOption, TimeZone, TIME_FORMAT } from '@grafana/data';
-import { SelectOptionItem } from '../Select/Select';
+import { TimeRange, TimeOption, TimeZone, TIME_FORMAT, SelectableValue } from '@grafana/data';
 
 export interface Props {
   value: TimeRange;
@@ -77,7 +76,7 @@ export class TimePicker extends PureComponent<Props, State> {
     isCustomOpen: false,
   };
 
-  mapTimeOptionsToSelectOptionItems = (selectOptions: TimeOption[]) => {
+  mapTimeOptionsToSelectableValues = (selectOptions: TimeOption[]) => {
     const options = selectOptions.map(timeOption => {
       return {
         label: timeOption.display,
@@ -93,7 +92,7 @@ export class TimePicker extends PureComponent<Props, State> {
     return options;
   };
 
-  onSelectChanged = (item: SelectOptionItem<TimeOption>) => {
+  onSelectChanged = (item: SelectableValue<TimeOption>) => {
     const { onChange, timeZone } = this.props;
 
     if (item.value && item.value.from === 'custom') {
@@ -122,7 +121,7 @@ export class TimePicker extends PureComponent<Props, State> {
   render() {
     const { selectOptions: selectTimeOptions, value, onMoveBackward, onMoveForward, onZoom, timeZone } = this.props;
     const { isCustomOpen } = this.state;
-    const options = this.mapTimeOptionsToSelectOptionItems(selectTimeOptions);
+    const options = this.mapTimeOptionsToSelectableValues(selectTimeOptions);
     const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value));
     const rangeString = rangeUtil.describeTimeRange(value.raw);
 

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

@@ -9,7 +9,7 @@ export * from './Button/Button';
 export { ButtonVariant } from './Button/AbstractButton';
 
 // Select
-export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
+export { Select, AsyncSelect } from './Select/Select';
 export { IndicatorsContainer } from './Select/IndicatorsContainer';
 export { NoOptionsMessage } from './Select/NoOptionsMessage';
 export { default as resetSelectStyles } from './Select/resetSelectStyles';

+ 3 - 2
public/app/core/components/PermissionList/AddPermission.tsx

@@ -1,7 +1,8 @@
 import React, { Component } from 'react';
 import { UserPicker } from 'app/core/components/Select/UserPicker';
 import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker';
-import { Select, SelectOptionItem } from '@grafana/ui';
+import { Select } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 import { User } from 'app/types';
 import {
   dashboardPermissionLevels,
@@ -61,7 +62,7 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
     this.setState({ teamId: team && !Array.isArray(team) ? team.id : 0 });
   };
 
-  onPermissionChanged = (permission: SelectOptionItem<PermissionLevel>) => {
+  onPermissionChanged = (permission: SelectableValue<PermissionLevel>) => {
     this.setState({ permission: permission.value });
   };
 

+ 3 - 2
public/app/core/components/PermissionList/PermissionListItem.tsx

@@ -1,5 +1,6 @@
 import React, { PureComponent } from 'react';
-import { Select, SelectOptionItem } from '@grafana/ui';
+import { Select } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
 import { FolderInfo } from 'app/types';
 
@@ -39,7 +40,7 @@ interface Props {
 }
 
 export default class PermissionsListItem extends PureComponent<Props> {
-  onPermissionChanged = (option: SelectOptionItem<PermissionLevel>) => {
+  onPermissionChanged = (option: SelectableValue<PermissionLevel>) => {
     this.props.onPermissionChanged(this.props.item, option.value);
   };
 

+ 3 - 2
public/app/core/components/Select/DataSourcePicker.tsx

@@ -2,7 +2,8 @@
 import React, { PureComponent } from 'react';
 
 // Components
-import { Select, SelectOptionItem } from '@grafana/ui';
+import { Select } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 
 // Types
 import { DataSourceSelectItem } from '@grafana/ui';
@@ -28,7 +29,7 @@ export class DataSourcePicker extends PureComponent<Props> {
     super(props);
   }
 
-  onChange = (item: SelectOptionItem<string>) => {
+  onChange = (item: SelectableValue<string>) => {
     const ds = this.props.datasources.find(ds => ds.name === item.value);
     this.props.onChange(ds);
   };

+ 4 - 3
public/app/core/components/Select/MetricSelect.tsx

@@ -1,12 +1,13 @@
 import React from 'react';
 import _ from 'lodash';
 
-import { Select, SelectOptionItem } from '@grafana/ui';
+import { Select } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 import { Variable } from 'app/types/templates';
 
 export interface Props {
   onChange: (value: string) => void;
-  options: Array<SelectOptionItem<string>>;
+  options: Array<SelectableValue<string>>;
   isSearchable: boolean;
   value: string;
   placeholder?: string;
@@ -15,7 +16,7 @@ export interface Props {
 }
 
 interface State {
-  options: Array<SelectOptionItem<string>>;
+  options: Array<SelectableValue<string>>;
 }
 
 export class MetricSelect extends React.Component<Props, State> {

+ 3 - 2
public/app/features/explore/AdHocFilter.tsx

@@ -1,6 +1,7 @@
 import React, { useContext } from 'react';
-import { Select, GrafanaTheme, ThemeContext, SelectOptionItem } from '@grafana/ui';
+import { Select, GrafanaTheme, ThemeContext } from '@grafana/ui';
 import { css, cx } from 'emotion';
+import { SelectableValue } from '@grafana/data';
 
 const getStyles = (theme: GrafanaTheme) => ({
   keyValueContainer: css`
@@ -33,7 +34,7 @@ export const AdHocFilter: React.FunctionComponent<Props> = props => {
   const theme = useContext(ThemeContext);
   const styles = getStyles(theme);
 
-  const onChange = (changeType: ChangeType) => (item: SelectOptionItem<string>) => {
+  const onChange = (changeType: ChangeType) => (item: SelectableValue<string>) => {
     const { onKeyChanged, onValueChanged, onOperatorChanged } = props;
     switch (changeType) {
       case ChangeType.Key:

+ 5 - 5
public/app/features/explore/ExploreToolbar.tsx

@@ -3,8 +3,8 @@ import { connect } from 'react-redux';
 import { hot } from 'react-hot-loader';
 
 import { ExploreId, ExploreMode } from 'app/types/explore';
-import { DataSourceSelectItem, SelectOptionItem } from '@grafana/ui';
-import { RawTimeRange, TimeZone, TimeRange, LoadingState } from '@grafana/data';
+import { DataSourceSelectItem } from '@grafana/ui';
+import { RawTimeRange, TimeZone, TimeRange, LoadingState, SelectableValue } from '@grafana/data';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { StoreState } from 'app/types/store';
 import {
@@ -67,8 +67,8 @@ interface StateProps {
   selectedDatasource: DataSourceSelectItem;
   splitted: boolean;
   refreshInterval: string;
-  supportedModeOptions: Array<SelectOptionItem<ExploreMode>>;
-  selectedModeOption: SelectOptionItem<ExploreMode>;
+  supportedModeOptions: Array<SelectableValue<ExploreMode>>;
+  selectedModeOption: SelectableValue<ExploreMode>;
   hasLiveOption: boolean;
   isLive: boolean;
 }
@@ -258,7 +258,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
   const hasLiveOption =
     datasourceInstance && datasourceInstance.meta && datasourceInstance.meta.streaming ? true : false;
 
-  const supportedModeOptions: Array<SelectOptionItem<ExploreMode>> = [];
+  const supportedModeOptions: Array<SelectableValue<ExploreMode>> = [];
   let selectedModeOption = null;
   for (const supportedMode of supportedModes) {
     switch (supportedMode) {

+ 2 - 2
public/app/features/teams/TeamMemberRow.test.tsx

@@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
 import { TeamMember, TeamPermissionLevel } from '../../types';
 import { getMockTeamMember } from './__mocks__/teamMocks';
 import { TeamMemberRow, Props } from './TeamMemberRow';
-import { SelectOptionItem } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 
 const setup = (propOverrides?: object) => {
   const props: Props = {
@@ -80,7 +80,7 @@ describe('Functions', () => {
     };
     const { instance } = setup({ member });
     const permission = TeamPermissionLevel.Admin;
-    const item: SelectOptionItem<TeamPermissionLevel> = { value: permission };
+    const item: SelectableValue<TeamPermissionLevel> = { value: permission };
     const expectedTeamMemeber = { ...member, permission };
 
     instance.onPermissionChange(item, member);

+ 3 - 2
public/app/features/teams/TeamMemberRow.tsx

@@ -1,6 +1,7 @@
 import React, { PureComponent } from 'react';
 import { connect } from 'react-redux';
-import { DeleteButton, Select, SelectOptionItem } from '@grafana/ui';
+import { DeleteButton, Select } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 
 import { TeamMember, teamsPermissionLevels, TeamPermissionLevel } from 'app/types';
 import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
@@ -27,7 +28,7 @@ export class TeamMemberRow extends PureComponent<Props> {
     this.props.removeTeamMember(member.userId);
   }
 
-  onPermissionChange = (item: SelectOptionItem<TeamPermissionLevel>, member: TeamMember) => {
+  onPermissionChange = (item: SelectableValue<TeamPermissionLevel>, member: TeamMember) => {
     const permission = item.value;
     const updatedTeamMember = { ...member, permission };
 

+ 3 - 3
public/app/plugins/datasource/input/InputQueryEditor.tsx

@@ -5,8 +5,8 @@ import React, { PureComponent } from 'react';
 import { InputDatasource, describeDataFrame } from './InputDatasource';
 import { InputQuery, InputOptions } from './types';
 
-import { FormLabel, Select, QueryEditorProps, SelectOptionItem, TableInputCSV } from '@grafana/ui';
-import { DataFrame, toCSV } from '@grafana/data';
+import { FormLabel, Select, QueryEditorProps, TableInputCSV } from '@grafana/ui';
+import { DataFrame, toCSV, SelectableValue } from '@grafana/data';
 
 type Props = QueryEditorProps<InputDatasource, InputQuery, InputOptions>;
 
@@ -30,7 +30,7 @@ export class InputQueryEditor extends PureComponent<Props, State> {
     this.setState({ text });
   }
 
-  onSourceChange = (item: SelectOptionItem<string>) => {
+  onSourceChange = (item: SelectableValue<string>) => {
     const { datasource, query, onChange, onRunQuery } = this.props;
     let data: DataFrame[] | undefined = undefined;
     if (item.value === 'panel') {

+ 2 - 5
public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx

@@ -1,9 +1,6 @@
 // Libraries
 import React, { PureComponent } from 'react';
 
-// Components
-// import { Select, SelectOptionItem } from '@grafana/ui';
-
 // Types
 import { QueryEditorProps } from '@grafana/ui';
 import { LokiDatasource } from '../datasource';
@@ -37,7 +34,7 @@ export class LokiQueryEditor extends PureComponent<Props> {
   //   });
   // };
   //
-  // onFormatChanged = (option: SelectOptionItem) => {
+  // onFormatChanged = (option: SelectableValue) => {
   //   this.props.onChange({
   //     ...this.state.query,
   //     resultFormat: option.value,
@@ -47,7 +44,7 @@ export class LokiQueryEditor extends PureComponent<Props> {
   render() {
     // const { query } = this.state;
     // const { datasource } = this.props;
-    // const formatOptions: SelectOptionItem[] = [
+    // const formatOptions: SelectableValue[] = [
     //   { label: 'Time Series', value: 'time_series' },
     //   { label: 'Table', value: 'table' },
     // ];

+ 8 - 9
public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx

@@ -2,33 +2,32 @@ import _ from 'lodash';
 import React, { PureComponent } from 'react';
 
 // Types
-import { FormLabel, Select, SelectOptionItem, Switch } from '@grafana/ui';
-import { QueryEditorProps, DataSourceStatus } from '@grafana/ui';
+import { FormLabel, Select, Switch, QueryEditorProps, DataSourceStatus } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 
 import { PrometheusDatasource } from '../datasource';
 import { PromQuery, PromOptions } from '../types';
 
 import PromQueryField from './PromQueryField';
 import PromLink from './PromLink';
-
 export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>;
 
-const FORMAT_OPTIONS: Array<SelectOptionItem<string>> = [
+const FORMAT_OPTIONS: Array<SelectableValue<string>> = [
   { label: 'Time series', value: 'time_series' },
   { label: 'Table', value: 'table' },
   { label: 'Heatmap', value: 'heatmap' },
 ];
 
-const INTERVAL_FACTOR_OPTIONS: Array<SelectOptionItem<number>> = _.map([1, 2, 3, 4, 5, 10], (value: number) => ({
+const INTERVAL_FACTOR_OPTIONS: Array<SelectableValue<number>> = _.map([1, 2, 3, 4, 5, 10], (value: number) => ({
   value,
   label: '1/' + value,
 }));
 
 interface State {
   legendFormat: string;
-  formatOption: SelectOptionItem<string>;
+  formatOption: SelectableValue<string>;
   interval: string;
-  intervalFactorOption: SelectOptionItem<number>;
+  intervalFactorOption: SelectableValue<number>;
   instant: boolean;
 }
 
@@ -58,7 +57,7 @@ export class PromQueryEditor extends PureComponent<Props, State> {
     this.query.expr = query.expr;
   };
 
-  onFormatChange = (option: SelectOptionItem<string>) => {
+  onFormatChange = (option: SelectableValue<string>) => {
     this.query.format = option.value;
     this.setState({ formatOption: option }, this.onRunQuery);
   };
@@ -75,7 +74,7 @@ export class PromQueryEditor extends PureComponent<Props, State> {
     this.setState({ interval });
   };
 
-  onIntervalFactorChange = (option: SelectOptionItem<number>) => {
+  onIntervalFactorChange = (option: SelectableValue<number>) => {
     this.query.intervalFactor = option.value;
     this.setState({ intervalFactorOption: option }, this.onRunQuery);
   };

+ 2 - 2
public/app/plugins/datasource/stackdriver/components/Alignments.tsx

@@ -3,12 +3,12 @@ import _ from 'lodash';
 
 import { MetricSelect } from 'app/core/components/Select/MetricSelect';
 import { TemplateSrv } from 'app/features/templating/template_srv';
-import { SelectOptionItem } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 
 export interface Props {
   onChange: (perSeriesAligner: any) => void;
   templateSrv: TemplateSrv;
-  alignOptions: Array<SelectOptionItem<string>>;
+  alignOptions: Array<SelectableValue<string>>;
   perSeriesAligner: string;
 }
 

+ 2 - 3
public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx

@@ -13,8 +13,7 @@ import { Help } from './Help';
 import { StackdriverQuery, MetricDescriptor } from '../types';
 import { getAlignmentPickerData } from '../functions';
 import StackdriverDatasource from '../datasource';
-import { SelectOptionItem } from '@grafana/ui';
-import { TimeSeries } from '@grafana/data';
+import { TimeSeries, SelectableValue } from '@grafana/data';
 
 export interface Props {
   onQueryChange: (target: StackdriverQuery) => void;
@@ -26,7 +25,7 @@ export interface Props {
 }
 
 interface State extends StackdriverQuery {
-  alignOptions: Array<SelectOptionItem<string>>;
+  alignOptions: Array<SelectableValue<string>>;
   lastQuery: string;
   lastQueryError: string;
   [key: string]: any;

+ 3 - 2
public/app/plugins/datasource/testdata/QueryEditor.tsx

@@ -6,7 +6,8 @@ import _ from 'lodash';
 import { getBackendSrv } from '@grafana/runtime';
 
 // Components
-import { FormLabel, Select, SelectOptionItem } from '@grafana/ui';
+import { FormLabel, Select } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 
 // Types
 import { QueryEditorProps } from '@grafana/ui';
@@ -40,7 +41,7 @@ export class QueryEditor extends PureComponent<Props> {
     this.setState({ scenarioList: scenarioList, current: current });
   }
 
-  onScenarioChange = (item: SelectOptionItem<string>) => {
+  onScenarioChange = (item: SelectableValue<string>) => {
     this.props.onChange({
       ...this.props.query,
       scenarioId: item.value,

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

@@ -1,17 +1,18 @@
-import { VizOrientation, SelectOptionItem, SingleStatBaseOptions } from '@grafana/ui';
+import { VizOrientation, SingleStatBaseOptions } from '@grafana/ui';
 import { standardGaugeFieldOptions } from '../gauge/types';
+import { SelectableValue } from '@grafana/data';
 
 export interface BarGaugeOptions extends SingleStatBaseOptions {
   displayMode: 'basic' | 'lcd' | 'gradient';
 }
 
-export const displayModes: Array<SelectOptionItem<string>> = [
+export const displayModes: Array<SelectableValue<string>> = [
   { value: 'gradient', label: 'Gradient' },
   { value: 'lcd', label: 'Retro LCD' },
   { value: 'basic', label: 'Basic' },
 ];
 
-export const orientationOptions: Array<SelectOptionItem<VizOrientation>> = [
+export const orientationOptions: Array<SelectableValue<VizOrientation>> = [
   { value: VizOrientation.Horizontal, label: 'Horizontal' },
   { value: VizOrientation.Vertical, label: 'Vertical' },
 ];

+ 3 - 4
public/app/plugins/panel/gauge/GaugeMigrations.ts

@@ -1,11 +1,10 @@
-import { Field, getFieldReducers } from '@grafana/data';
-import { PanelModel } from '@grafana/ui';
+import { Field, fieldReducers } from '@grafana/data';
+import { PanelModel, FieldDisplayOptions } from '@grafana/ui';
 import { GaugeOptions } from './types';
 import {
   sharedSingleStatMigrationCheck,
   migrateOldThresholds,
 } 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) {
@@ -33,7 +32,7 @@ export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Parti
 
     // Make sure the stats have a valid name
     if (valueOptions.stat) {
-      fieldOptions.calcs = getFieldReducers([valueOptions.stat]).map(s => s.id);
+      fieldOptions.calcs = [fieldReducers.get(valueOptions.stat).id];
     }
     field.min = old.minValue;
     field.max = old.maxValue;

+ 5 - 4
public/app/plugins/panel/singlestat2/FontSizeEditor.tsx

@@ -2,10 +2,11 @@
 import React, { PureComponent } from 'react';
 
 // Components
-import { FormLabel, Select, PanelOptionsGroup, SelectOptionItem } from '@grafana/ui';
+import { FormLabel, Select, PanelOptionsGroup } from '@grafana/ui';
 
 // Types
 import { SingleStatOptions } from './types';
+import { SelectableValue } from '@grafana/data';
 
 const labelWidth = 6;
 
@@ -20,13 +21,13 @@ const fontSizeOptions = percents.map(v => {
 });
 
 export class FontSizeEditor extends PureComponent<Props> {
-  setPrefixFontSize = (v: SelectOptionItem<string>) =>
+  setPrefixFontSize = (v: SelectableValue<string>) =>
     this.props.onChange({ ...this.props.options, prefixFontSize: v.value });
 
-  setValueFontSize = (v: SelectOptionItem<string>) =>
+  setValueFontSize = (v: SelectableValue<string>) =>
     this.props.onChange({ ...this.props.options, valueFontSize: v.value });
 
-  setPostfixFontSize = (v: SelectOptionItem<string>) =>
+  setPostfixFontSize = (v: SelectableValue<string>) =>
     this.props.onChange({ ...this.props.options, postfixFontSize: v.value });
 
   render() {

+ 4 - 3
public/app/plugins/panel/text2/TextPanelEditor.tsx

@@ -2,19 +2,20 @@
 import React, { PureComponent, ChangeEvent } from 'react';
 
 // Components
-import { PanelEditorProps, PanelOptionsGroup, Select, SelectOptionItem } from '@grafana/ui';
+import { PanelEditorProps, PanelOptionsGroup, Select } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
 
 // Types
 import { TextOptions, TextMode } from './types';
 
 export class TextPanelEditor extends PureComponent<PanelEditorProps<TextOptions>> {
-  modes: Array<SelectOptionItem<TextMode>> = [
+  modes: Array<SelectableValue<TextMode>> = [
     { value: 'markdown', label: 'Markdown' },
     { value: 'text', label: 'Text' },
     { value: 'html', label: 'HTML' },
   ];
 
-  onModeChange = (item: SelectOptionItem<TextMode>) =>
+  onModeChange = (item: SelectableValue<TextMode>) =>
     this.props.onOptionsChange({ ...this.props.options, mode: item.value });
 
   onContentChange = (evt: ChangeEvent<HTMLTextAreaElement>) => {