ryan 6 лет назад
Родитель
Сommit
fe22d14e93

+ 17 - 9
packages/grafana-ui/src/components/Table/Table.story.tsx

@@ -5,24 +5,32 @@ import { Table } from './Table';
 import { migratedTestTable, migratedTestStyles, simpleTable } from './examples';
 import { ScopedVars, TableData } from '../../types/index';
 import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory';
+import { number, boolean } from '@storybook/addon-knobs';
 
-const replaceVariables = (value: any, scopedVars: ScopedVars | undefined) => {
-  // if (scopedVars) {
-  //   // For testing variables replacement in link
-  //   _.each(scopedVars, (val, key) => {
-  //     value = value.replace('$' + key, val.value);
-  //   });
-  // }
+const replaceVariables = (value: string, scopedVars?: ScopedVars) => {
+  if (scopedVars) {
+    // For testing variables replacement in link
+    for (const key in scopedVars) {
+      const val = scopedVars[key];
+      value = value.replace('$' + key, val.value);
+    }
+  }
   return value;
 };
 
-storiesOf('UI - Alpha/Table', module)
+storiesOf('UI/Table', module)
   .add('basic', () => {
+    const showHeader = boolean('Show Header', true);
+    const fixedRowCount = number('Fixed Rows', 1);
+    const fixedColumnCount = number('Fixed Columns', 1);
+
     return withFullSizeStory(Table, {
       styles: [],
       data: simpleTable,
       replaceVariables,
-      showHeader: true,
+      fixedRowCount,
+      fixedColumnCount,
+      showHeader,
     });
   })
   .add('Test Configuration', () => {

+ 0 - 240
packages/grafana-ui/src/components/Table/Table.test.ts

@@ -1,240 +0,0 @@
-import _ from 'lodash';
-
-import { getColorDefinitionByName } from '@grafana/ui';
-import { ScopedVars } from '@grafana/ui/src/types';
-import { getTheme } from '../../themes';
-
-import { migratedTestTable, migratedTestStyles } from './examples';
-import TableXXXX from './TableXXXX';
-
-// TODO: this is commented out with *x* describe!
-// Essentially all the elements need to replace the <td> with <div>
-xdescribe('when rendering table', () => {
-  const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
-
-  describe('given 13 columns', () => {
-    // const sanitize = value => {
-    //   return 'sanitized';
-    // };
-
-    const replaceVariables = (value: any, scopedVars: ScopedVars | undefined) => {
-      if (scopedVars) {
-        // For testing variables replacement in link
-        _.each(scopedVars, (val, key) => {
-          value = value.replace('$' + key, val.value);
-        });
-      }
-      return value;
-    };
-
-    const table = migratedTestTable;
-    const renderer = new TableXXXX({
-      styles: migratedTestStyles,
-      data: migratedTestTable,
-      replaceVariables,
-      showHeader: true,
-      width: 100,
-      height: 100,
-      theme: getTheme(),
-    });
-
-    it('time column should be formated', () => {
-      const html = renderer.renderCell(0, 0, 1388556366666);
-      expect(html).toBe('<td>2014-01-01T06:06:06Z</td>');
-    });
-
-    it('time column with epoch as string should be formatted', () => {
-      const html = renderer.renderCell(0, 0, '1388556366666');
-      expect(html).toBe('<td>2014-01-01T06:06:06Z</td>');
-    });
-
-    it('time column with RFC2822 date as string should be formatted', () => {
-      const html = renderer.renderCell(0, 0, 'Sat, 01 Dec 2018 01:00:00 GMT');
-      expect(html).toBe('<td>2018-12-01T01:00:00Z</td>');
-    });
-
-    it('time column with ISO date as string should be formatted', () => {
-      const html = renderer.renderCell(0, 0, '2018-12-01T01:00:00Z');
-      expect(html).toBe('<td>2018-12-01T01:00:00Z</td>');
-    });
-
-    it('undefined time column should be rendered as -', () => {
-      const html = renderer.renderCell(0, 0, undefined);
-      expect(html).toBe('<td>-</td>');
-    });
-
-    it('null time column should be rendered as -', () => {
-      const html = renderer.renderCell(0, 0, null);
-      expect(html).toBe('<td>-</td>');
-    });
-
-    it('number column with unit specified should ignore style unit', () => {
-      const html = renderer.renderCell(5, 0, 1230);
-      expect(html).toBe('<td>1.23 kbps</td>');
-    });
-
-    it('number column should be formated', () => {
-      const html = renderer.renderCell(1, 0, 1230);
-      expect(html).toBe('<td>1.230 s</td>');
-    });
-
-    it('number style should ignore string values', () => {
-      const html = renderer.renderCell(1, 0, 'asd');
-      expect(html).toBe('<td>asd</td>');
-    });
-
-    it('colored cell should have style (handles HEX color values)', () => {
-      const html = renderer.renderCell(2, 0, 40);
-      expect(html).toBe('<td style="color:#00ff00">40.0</td>');
-    });
-
-    it('colored cell should have style (handles named color values', () => {
-      const html = renderer.renderCell(2, 0, 55);
-      expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">55.0</td>`);
-    });
-
-    it('colored cell should have style handles(rgb color values)', () => {
-      const html = renderer.renderCell(2, 0, 85);
-      expect(html).toBe('<td style="color:rgb(1,0,0)">85.0</td>');
-    });
-
-    it('unformated undefined should be rendered as string', () => {
-      const html = renderer.renderCell(3, 0, 'value');
-      expect(html).toBe('<td>value</td>');
-    });
-
-    it('string style with escape html should return escaped html', () => {
-      const html = renderer.renderCell(4, 0, '&breaking <br /> the <br /> row');
-      expect(html).toBe('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
-    });
-
-    it('undefined formater should return escaped html', () => {
-      const html = renderer.renderCell(3, 0, '&breaking <br /> the <br /> row');
-      expect(html).toBe('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
-    });
-
-    it('undefined value should render as -', () => {
-      const html = renderer.renderCell(3, 0, undefined);
-      expect(html).toBe('<td></td>');
-    });
-
-    it('sanitized value should render as', () => {
-      const html = renderer.renderCell(6, 0, 'text <a href="http://google.com">link</a>');
-      expect(html).toBe('<td>sanitized</td>');
-    });
-
-    it('Time column title should be Timestamp', () => {
-      expect(table.columns[0].title).toBe('Timestamp');
-    });
-
-    it('Value column title should be Val', () => {
-      expect(table.columns[1].title).toBe('Val');
-    });
-
-    it('Colored column title should be Colored', () => {
-      expect(table.columns[2].title).toBe('Colored');
-    });
-
-    it('link should render as', () => {
-      const html = renderer.renderCell(7, 0, 'host1');
-      const expectedHtml = `
-        <td class="table-panel-cell-link">
-          <a href="/dashboard?param=host1&param_1=1230&param_2=40"
-            target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right">
-            host1
-          </a>
-        </td>
-      `;
-      expect(normalize(html + '')).toBe(normalize(expectedHtml));
-    });
-
-    it('Array column should not use number as formatter', () => {
-      const html = renderer.renderCell(8, 0, ['value1', 'value2']);
-      expect(html).toBe('<td>value1, value2</td>');
-    });
-
-    it('numeric value should be mapped to text', () => {
-      const html = renderer.renderCell(9, 0, 1);
-      expect(html).toBe('<td>on</td>');
-    });
-
-    it('string numeric value should be mapped to text', () => {
-      const html = renderer.renderCell(9, 0, '0');
-      expect(html).toBe('<td>off</td>');
-    });
-
-    it('string value should be mapped to text', () => {
-      const html = renderer.renderCell(9, 0, 'HELLO WORLD');
-      expect(html).toBe('<td>HELLO GRAFANA</td>');
-    });
-
-    it('array column value should be mapped to text', () => {
-      const html = renderer.renderCell(9, 0, ['value1', 'value2']);
-      expect(html).toBe('<td>value3, value4</td>');
-    });
-
-    it('value should be mapped to text (range)', () => {
-      const html = renderer.renderCell(10, 0, 2);
-      expect(html).toBe('<td>on</td>');
-    });
-
-    it('value should be mapped to text (range)', () => {
-      const html = renderer.renderCell(10, 0, 5);
-      expect(html).toBe('<td>off</td>');
-    });
-
-    it('array column value should not be mapped to text', () => {
-      const html = renderer.renderCell(10, 0, ['value1', 'value2']);
-      expect(html).toBe('<td>value1, value2</td>');
-    });
-
-    it('value should be mapped to text and colored cell should have style', () => {
-      const html = renderer.renderCell(11, 0, 1);
-      expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">on</td>`);
-    });
-
-    it('value should be mapped to text and colored cell should have style', () => {
-      const html = renderer.renderCell(11, 0, '1');
-      expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">on</td>`);
-    });
-
-    it('value should be mapped to text and colored cell should have style', () => {
-      const html = renderer.renderCell(11, 0, 0);
-      expect(html).toBe('<td style="color:#00ff00">off</td>');
-    });
-
-    it('value should be mapped to text and colored cell should have style', () => {
-      const html = renderer.renderCell(11, 0, '0');
-      expect(html).toBe('<td style="color:#00ff00">off</td>');
-    });
-
-    it('value should be mapped to text and colored cell should have style', () => {
-      const html = renderer.renderCell(11, 0, '2.1');
-      expect(html).toBe('<td style="color:rgb(1,0,0)">2.1</td>');
-    });
-
-    it('value should be mapped to text (range) and colored cell should have style', () => {
-      const html = renderer.renderCell(12, 0, 0);
-      expect(html).toBe('<td style="color:#00ff00">0</td>');
-    });
-
-    it('value should be mapped to text (range) and colored cell should have style', () => {
-      const html = renderer.renderCell(12, 0, 1);
-      expect(html).toBe('<td style="color:#00ff00">on</td>');
-    });
-
-    it('value should be mapped to text (range) and colored cell should have style', () => {
-      const html = renderer.renderCell(12, 0, 4);
-      expect(html).toBe(`<td style="color:${SemiDarkOrange.variants.dark}">off</td>`);
-    });
-
-    it('value should be mapped to text (range) and colored cell should have style', () => {
-      const html = renderer.renderCell(12, 0, '7.1');
-      expect(html).toBe('<td style="color:rgb(1,0,0)">7.1</td>');
-    });
-  });
-});
-
-function normalize(str: string) {
-  return str.replace(/\s+/gm, ' ').trim();
-}

+ 61 - 86
packages/grafana-ui/src/components/Table/Table.tsx

@@ -1,6 +1,6 @@
 // Libraries
 import _ from 'lodash';
-import React, { Component, ReactNode } from 'react';
+import React, { Component, ReactElement } from 'react';
 import {
   SortDirectionType,
   SortIndicator,
@@ -14,49 +14,16 @@ import { Themeable } from '../../types/theme';
 import { sortTableData } from '../../utils/processTimeSeries';
 
 import { TableData, InterpolateFunction } from '@grafana/ui';
-import { ColumnStyle } from './Table';
-
-// APP Imports!!!
-// import kbn from 'app/core/utils/kbn';
-
-// Made to match the existing (untyped) settings in the angular table
-export interface ColumnStyle {
-  pattern?: string;
-
-  alias?: string;
-  colorMode?: 'cell' | 'value';
-  colors?: any[];
-  decimals?: number;
-  thresholds?: any[];
-  type?: 'date' | 'number' | 'string' | 'hidden';
-  unit?: string;
-  dateFormat?: string;
-  sanitize?: boolean; // not used in react
-  mappingType?: any;
-  valueMaps?: any;
-  rangeMaps?: any;
-
-  link?: any;
-  linkUrl?: any;
-  linkTooltip?: any;
-  linkTargetBlank?: boolean;
-
-  preserveFormat?: boolean;
-}
-
-type CellFormatter = (v: any, style?: ColumnStyle) => ReactNode;
+import { TableCellBuilder, ColumnStyle, getCellBuilder, TableCellBuilderOptions } from './TableCellBuilder';
 
 interface ColumnInfo {
+  index: number;
   header: string;
-  accessor: string; // the field name
-  style?: ColumnStyle;
-  hidden?: boolean;
-  formatter: CellFormatter;
-  filterable?: boolean;
+  builder: TableCellBuilder;
 }
 
-interface Props extends Themeable {
-  data?: TableData;
+export interface Props extends Themeable {
+  data: TableData;
   showHeader: boolean;
   fixedColumnCount: number;
   fixedRowCount: number;
@@ -70,14 +37,12 @@ interface Props extends Themeable {
 interface State {
   sortBy?: number;
   sortDirection?: SortDirectionType;
-  data?: TableData;
+  data: TableData;
 }
 
 export class Table extends Component<Props, State> {
-  columns: ColumnInfo[] = [];
-  colorState: any;
-
-  _cache: CellMeasurerCache;
+  columns: ColumnInfo[];
+  measurer: CellMeasurerCache;
 
   static defaultProps = {
     showHeader: true,
@@ -92,12 +57,11 @@ export class Table extends Component<Props, State> {
       data: props.data,
     };
 
-    this._cache = new CellMeasurerCache({
+    this.columns = this.initColumns(props);
+    this.measurer = new CellMeasurerCache({
       defaultHeight: 30,
       defaultWidth: 150,
     });
-
-    this.initRenderer();
   }
 
   componentDidUpdate(prevProps: Props, prevState: State) {
@@ -105,9 +69,14 @@ export class Table extends Component<Props, State> {
     const { sortBy, sortDirection } = this.state;
     const dataChanged = data !== prevProps.data;
 
+    // Reset the size cache
+    if (dataChanged) {
+      this.measurer.clearAll();
+    }
+
     // Update the renderer if options change
     if (dataChanged || styles !== prevProps.styles) {
-      this.initRenderer();
+      this.columns = this.initColumns(this.props);
     }
 
     // Update the data when data or sort changes
@@ -117,7 +86,32 @@ export class Table extends Component<Props, State> {
     }
   }
 
-  initRenderer() {}
+  initColumns(props: Props): ColumnInfo[] {
+    const { styles, data } = props;
+    return data.columns.map((col, index) => {
+      let title = col.text;
+      let style: ColumnStyle | null = null; // ColumnStyle
+
+      // Find the style based on the text
+      for (let i = 0; i < styles.length; i++) {
+        const s = styles[i];
+        const regex = 'XXX'; //kbn.stringToJsRegex(s.pattern);
+        if (title.match(regex)) {
+          style = s;
+          if (s.alias) {
+            title = title.replace(regex, s.alias);
+          }
+          break;
+        }
+      }
+
+      return {
+        index,
+        header: title,
+        builder: getCellBuilder(col, style, this.props),
+      };
+    });
+  }
 
   //----------------------------------------------------------------------
   //----------------------------------------------------------------------
@@ -136,7 +130,7 @@ export class Table extends Component<Props, State> {
     this.setState({ sortBy: sort, sortDirection: dir });
   };
 
-  handelClick = (rowIndex: number, columnIndex: number) => {
+  handleCellClick = (rowIndex: number, columnIndex: number) => {
     const { showHeader } = this.props;
     const { data } = this.state;
     const realRowIndex = rowIndex - (showHeader ? 1 : 0);
@@ -149,14 +143,16 @@ export class Table extends Component<Props, State> {
     }
   };
 
-  headerRenderer = (columnIndex: number): ReactNode => {
+  headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => {
     const { data, sortBy, sortDirection } = this.state;
+    const { columnIndex, rowIndex, style } = cell.props;
+
     const col = data!.columns[columnIndex];
     const sorting = sortBy === columnIndex;
 
     return (
-      <div>
-        {col.text}{' '}
+      <div className="gf-table-header" style={style} onClick={() => this.handleCellClick(rowIndex, columnIndex)}>
+        {col.text}
         {sorting && (
           <span>
             {sortDirection}
@@ -168,43 +164,22 @@ export class Table extends Component<Props, State> {
   };
 
   cellRenderer = (props: GridCellProps): React.ReactNode => {
-    const { rowIndex, columnIndex, key, parent, style } = props;
+    const { rowIndex, columnIndex, key, parent } = props;
     const { showHeader } = this.props;
     const { data } = this.state;
     if (!data) {
-      return <div>?</div>;
+      return <div>??</div>;
     }
 
     const realRowIndex = rowIndex - (showHeader ? 1 : 0);
-
-    let classNames = 'gf-table-cell';
-    let content = null;
-
-    if (realRowIndex < 0) {
-      content = this.headerRenderer(columnIndex);
-      classNames = 'gf-table-header';
-    } else {
-      const row = data.rows[realRowIndex];
-      const value = row[columnIndex];
-      content = (
-        <div>
-          {rowIndex}/{columnIndex}: {value}
-        </div>
-      );
-    }
+    const isHeader = realRowIndex < 0;
+    const row = isHeader ? (data.columns as any[]) : data.rows[realRowIndex];
+    const value = row[columnIndex];
+    const builder = isHeader ? this.headerBuilder : this.columns[columnIndex].builder;
 
     return (
-      <CellMeasurer cache={this._cache} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
-        <div
-          onClick={() => this.handelClick(rowIndex, columnIndex)}
-          className={classNames}
-          style={{
-            ...style,
-            whiteSpace: 'nowrap',
-          }}
-        >
-          {content}
-        </div>
+      <CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
+        {builder({ value, row, table: this, props })}
       </CellMeasurer>
     );
   };
@@ -218,16 +193,16 @@ export class Table extends Component<Props, State> {
     return (
       <MultiGrid
         {
-          ...this.state /** Force MultiGrid to update when any property updates */
+          ...this.state /** Force MultiGrid to update when data changes */
         }
         columnCount={data.columns.length}
         rowCount={data.rows.length + (showHeader ? 1 : 0)}
         overscanColumnCount={2}
         overscanRowCount={2}
-        columnWidth={this._cache.columnWidth}
-        deferredMeasurementCache={this._cache}
+        columnWidth={this.measurer.columnWidth}
+        deferredMeasurementCache={this.measurer}
         cellRenderer={this.cellRenderer}
-        rowHeight={this._cache.rowHeight}
+        rowHeight={this.measurer.rowHeight}
         width={width}
         height={height}
         fixedColumnCount={fixedColumnCount}

+ 292 - 0
packages/grafana-ui/src/components/Table/TableCellBuilder.tsx

@@ -0,0 +1,292 @@
+// Libraries
+import _ from 'lodash';
+import React, { ReactElement } from 'react';
+import { GridCellProps } from 'react-virtualized';
+import { Table, Props } from './Table';
+import moment from 'moment';
+import { ValueFormatter } from '../../utils/index';
+import { GrafanaTheme } from '../../types/theme';
+import { getValueFormat, getColorFromHexRgbOrName, Column } from '@grafana/ui';
+import { InterpolateFunction } from '../../types/panel';
+
+export interface TableCellBuilderOptions {
+  value: any;
+  row?: any[];
+  table?: Table;
+  className?: string;
+  props: GridCellProps;
+}
+
+export type TableCellBuilder = (cell: TableCellBuilderOptions) => ReactElement<'div'>;
+
+/** Simplest cell that just spits out the value */
+export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOptions) => {
+  const { props, value, className } = cell;
+  const { style } = props;
+
+  return (
+    <div style={style} className={className}>
+      {value}
+    </div>
+  );
+};
+
+// ***************************************************************************
+// HERE BE DRAGONS!!!
+// ***************************************************************************
+//
+//  The following code has been migrated blindy two times from the angular
+//  table panel.  I don't understand all the options nor do I know if they
+//  are correct!
+//
+// ***************************************************************************
+
+// APP Imports!!!
+// import kbn from 'app/core/utils/kbn';
+
+// Made to match the existing (untyped) settings in the angular table
+export interface ColumnStyle {
+  pattern?: string;
+
+  alias?: string;
+  colorMode?: 'cell' | 'value';
+  colors?: any[];
+  decimals?: number;
+  thresholds?: any[];
+  type?: 'date' | 'number' | 'string' | 'hidden';
+  unit?: string;
+  dateFormat?: string;
+  sanitize?: boolean; // not used in react
+  mappingType?: any;
+  valueMaps?: any;
+  rangeMaps?: any;
+
+  link?: any;
+  linkUrl?: any;
+  linkTooltip?: any;
+  linkTargetBlank?: boolean;
+
+  preserveFormat?: boolean;
+}
+
+// private mapper:ValueMapper,
+// private style:ColumnStyle,
+// private theme:GrafanaTheme,
+// private column:Column,
+// private replaceVariables: InterpolateFunction,
+// private fmt?:ValueFormatter) {
+
+export function getCellBuilder(schema: Column, style: ColumnStyle | null, props: Props): TableCellBuilder {
+  if (!style) {
+    return simpleCellBuilder;
+  }
+
+  if (style.type === 'hidden') {
+    // TODO -- for hidden, we either need to:
+    // 1. process the Table and remove hidden fields
+    // 2. do special math to pick the right column skipping hidden fields
+    throw new Error('hidden not supported!');
+  }
+
+  if (style.type === 'date') {
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (v === undefined || v === null) {
+          return '-';
+        }
+
+        if (_.isArray(v)) {
+          v = v[0];
+        }
+        let date = moment(v);
+        if (false) {
+          // TODO?????? this.props.isUTC) {
+          date = date.utc();
+        }
+        return date.format(style.dateFormat);
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables
+    ).build;
+  }
+
+  if (style.type === 'string') {
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (_.isArray(v)) {
+          v = v.join(', ');
+        }
+        return v;
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables
+    ).build;
+    // TODO!!!!  all the mapping stuff!!!!
+  }
+
+  if (style.type === 'number') {
+    const valueFormatter = getValueFormat(style.unit || schema.unit || 'none');
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (v === null || v === void 0) {
+          return '-';
+        }
+        return v;
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables,
+      valueFormatter
+    ).build;
+  }
+
+  return simpleCellBuilder;
+}
+
+type ValueMapper = (value: any) => any;
+
+// Runs the value through a formatter and adds colors to the cell properties
+class CellBuilderWithStyle {
+  constructor(
+    private mapper: ValueMapper,
+    private style: ColumnStyle,
+    private theme: GrafanaTheme,
+    private column: Column,
+    private replaceVariables: InterpolateFunction,
+    private fmt?: ValueFormatter
+  ) {
+    //
+  }
+
+  getColorForValue = (value: any): string | null => {
+    const { thresholds, colors } = this.style;
+    if (!thresholds || !colors) {
+      return null;
+    }
+
+    for (let i = thresholds.length; i > 0; i--) {
+      if (value >= thresholds[i - 1]) {
+        return getColorFromHexRgbOrName(colors[i], this.theme.type);
+      }
+    }
+    return getColorFromHexRgbOrName(_.first(colors), this.theme.type);
+  };
+
+  build = (cell: TableCellBuilderOptions) => {
+    let { props } = cell;
+    let value = this.mapper(cell.value);
+
+    if (_.isNumber(value)) {
+      if (this.fmt) {
+        value = this.fmt(value, this.style.decimals);
+      }
+
+      // For numeric values set the color
+      const { colorMode } = this.style;
+      if (colorMode) {
+        const color = this.getColorForValue(Number(value));
+        if (color) {
+          if (colorMode === 'cell') {
+            props = {
+              ...props,
+              style: {
+                ...props.style,
+                backgroundColor: color,
+                color: 'white',
+              },
+            };
+          } else if (colorMode === 'value') {
+            props = {
+              ...props,
+              style: {
+                ...props.style,
+                color: color,
+              },
+            };
+          }
+        }
+      }
+    }
+
+    const cellClasses = [];
+    if (this.style.preserveFormat) {
+      cellClasses.push('table-panel-cell-pre');
+    }
+
+    if (this.style.link) {
+      // Render cell as link
+      const { row } = cell;
+
+      const scopedVars: any = {};
+      if (row) {
+        for (let i = 0; i < row.length; i++) {
+          scopedVars[`__cell_${i}`] = { value: row[i] };
+        }
+      }
+      scopedVars['__cell'] = { value: value };
+
+      const cellLink = this.replaceVariables(this.style.linkUrl, scopedVars, encodeURIComponent);
+      const cellLinkTooltip = this.replaceVariables(this.style.linkTooltip, scopedVars);
+      const cellTarget = this.style.linkTargetBlank ? '_blank' : '';
+
+      cellClasses.push('table-panel-cell-link');
+      value = (
+        <a
+          href={cellLink}
+          target={cellTarget}
+          data-link-tooltip
+          data-original-title={cellLinkTooltip}
+          data-placement="right"
+        >
+          {value}
+        </a>
+      );
+    }
+
+    // ??? I don't think this will still work!
+    if (this.column.filterable) {
+      cellClasses.push('table-panel-cell-filterable');
+      value = (
+        <>
+          {value}
+          <span>
+            <a
+              className="table-panel-filter-link"
+              data-link-tooltip
+              data-original-title="Filter out value"
+              data-placement="bottom"
+              data-row={props.rowIndex}
+              data-column={props.columnIndex}
+              data-operator="!="
+            >
+              <i className="fa fa-search-minus" />
+            </a>
+            <a
+              className="table-panel-filter-link"
+              data-link-tooltip
+              data-original-title="Filter for value"
+              data-placement="bottom"
+              data-row={props.rowIndex}
+              data-column={props.columnIndex}
+              data-operator="="
+            >
+              <i className="fa fa-search-plus" />
+            </a>
+          </span>
+        </>
+      );
+    }
+
+    let className;
+    if (cellClasses.length) {
+      className = cellClasses.join(' ');
+    }
+
+    return simpleCellBuilder({ value, props, className });
+  };
+}

+ 0 - 456
packages/grafana-ui/src/components/Table/TableXXXX.tsx

@@ -1,456 +0,0 @@
-// Libraries
-import _ from 'lodash';
-import React, { Component, CSSProperties, ReactNode } from 'react';
-import {
-  Table as RVTable,
-  SortDirectionType,
-  SortIndicator,
-  Column as RVColumn,
-  TableHeaderProps,
-  TableCellProps,
-} from 'react-virtualized';
-import { Themeable } from '../../types/theme';
-
-import { sortTableData } from '../../utils/processTimeSeries';
-
-import moment from 'moment';
-
-import { getValueFormat, TableData, getColorFromHexRgbOrName, InterpolateFunction, Column } from '@grafana/ui';
-import { Index } from 'react-virtualized';
-import { ColumnStyle } from './Table';
-
-type CellFormatter = (v: any, style?: ColumnStyle) => ReactNode;
-
-interface ColumnInfo {
-  header: string;
-  accessor: string; // the field name
-  style?: ColumnStyle;
-  hidden?: boolean;
-  formatter: CellFormatter;
-  filterable?: boolean;
-}
-
-interface Props extends Themeable {
-  data?: TableData;
-  showHeader: boolean;
-  styles: ColumnStyle[];
-  replaceVariables: InterpolateFunction;
-  width: number;
-  height: number;
-  isUTC?: boolean;
-}
-
-interface State {
-  sortBy?: number;
-  sortDirection?: SortDirectionType;
-  data?: TableData;
-}
-
-export class TableXXXX extends Component<Props, State> {
-  columns: ColumnInfo[] = [];
-  colorState: any;
-
-  static defaultProps = {
-    showHeader: true,
-  };
-
-  constructor(props: Props) {
-    super(props);
-
-    this.state = {
-      data: props.data,
-    };
-
-    this.initRenderer();
-  }
-
-  componentDidUpdate(prevProps: Props, prevState: State) {
-    const { data, styles } = this.props;
-    const { sortBy, sortDirection } = this.state;
-    const dataChanged = data !== prevProps.data;
-
-    // Update the renderer if options change
-    if (dataChanged || styles !== prevProps.styles) {
-      this.initRenderer();
-    }
-
-    // Update the data when data or sort changes
-    if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) {
-      const sorted = data ? sortTableData(data, sortBy, sortDirection === 'DESC') : data;
-      this.setState({ data: sorted });
-    }
-  }
-
-  initRenderer() {
-    const { styles } = this.props;
-    const { data } = this.state;
-    this.colorState = {};
-    if (!data || !data.columns) {
-      this.columns = [];
-      return;
-    }
-    this.columns = data.columns.map((col, index) => {
-      let title = col.text;
-      let style; // ColumnStyle
-
-      // Find the style based on the text
-      for (let i = 0; i < styles.length; i++) {
-        const s = styles[i];
-        const regex = 'XXX'; //kbn.stringToJsRegex(s.pattern);
-        if (title.match(regex)) {
-          style = s;
-          if (s.alias) {
-            title = title.replace(regex, s.alias);
-          }
-          break;
-        }
-      }
-
-      return {
-        header: title,
-        accessor: col.text, // unique?
-        style: style,
-        formatter: this.createColumnFormatter(col, style),
-      };
-    });
-  }
-
-  //----------------------------------------------------------------------
-  // renderer.ts copy (taken from angular version!!!)
-  //----------------------------------------------------------------------
-
-  getColorForValue(value: any, style: ColumnStyle) {
-    if (!style.thresholds || !style.colors) {
-      return null;
-    }
-    const { theme } = this.props;
-
-    for (let i = style.thresholds.length; i > 0; i--) {
-      if (value >= style.thresholds[i - 1]) {
-        return getColorFromHexRgbOrName(style.colors[i], theme.type);
-      }
-    }
-    return getColorFromHexRgbOrName(_.first(style.colors), theme.type);
-  }
-
-  defaultCellFormatter(v: any, style?: ColumnStyle): string {
-    if (v === null || v === void 0 || v === undefined) {
-      return '';
-    }
-
-    if (_.isArray(v)) {
-      v = v.join(', ');
-    }
-
-    return v; // react will sanitize
-  }
-
-  createColumnFormatter(schema: Column, style?: ColumnStyle): CellFormatter {
-    if (!style) {
-      return this.defaultCellFormatter;
-    }
-
-    if (style.type === 'hidden') {
-      return v => {
-        return undefined;
-      };
-    }
-
-    if (style.type === 'date') {
-      return v => {
-        if (v === undefined || v === null) {
-          return '-';
-        }
-
-        if (_.isArray(v)) {
-          v = v[0];
-        }
-        let date = moment(v);
-        if (this.props.isUTC) {
-          date = date.utc();
-        }
-        return date.format(style.dateFormat);
-      };
-    }
-
-    if (style.type === 'string') {
-      return v => {
-        if (_.isArray(v)) {
-          v = v.join(', ');
-        }
-
-        const mappingType = style.mappingType || 0;
-
-        if (mappingType === 1 && style.valueMaps) {
-          for (let i = 0; i < style.valueMaps.length; i++) {
-            const map = style.valueMaps[i];
-
-            if (v === null) {
-              if (map.value === 'null') {
-                return map.text;
-              }
-              continue;
-            }
-
-            // Allow both numeric and string values to be mapped
-            if ((!_.isString(v) && Number(map.value) === Number(v)) || map.value === v) {
-              this.setColorState(v, style);
-              return this.defaultCellFormatter(map.text, style);
-            }
-          }
-        }
-
-        if (mappingType === 2 && style.rangeMaps) {
-          for (let i = 0; i < style.rangeMaps.length; i++) {
-            const map = style.rangeMaps[i];
-
-            if (v === null) {
-              if (map.from === 'null' && map.to === 'null') {
-                return map.text;
-              }
-              continue;
-            }
-
-            if (Number(map.from) <= Number(v) && Number(map.to) >= Number(v)) {
-              this.setColorState(v, style);
-              return this.defaultCellFormatter(map.text, style);
-            }
-          }
-        }
-
-        if (v === null || v === void 0) {
-          return '-';
-        }
-
-        this.setColorState(v, style);
-        return this.defaultCellFormatter(v, style);
-      };
-    }
-
-    if (style.type === 'number') {
-      const valueFormatter = getValueFormat(style.unit || schema.unit || 'none');
-
-      return v => {
-        if (v === null || v === void 0) {
-          return '-';
-        }
-
-        if (_.isString(v) || _.isArray(v)) {
-          return this.defaultCellFormatter(v, style);
-        }
-
-        this.setColorState(v, style);
-        return valueFormatter(v, style.decimals, null);
-      };
-    }
-
-    return value => {
-      return this.defaultCellFormatter(value, style);
-    };
-  }
-
-  setColorState(value: any, style: ColumnStyle) {
-    if (!style.colorMode) {
-      return;
-    }
-
-    if (value === null || value === void 0 || _.isArray(value)) {
-      return;
-    }
-
-    if (_.isNaN(value)) {
-      return;
-    }
-    const numericValue = Number(value);
-    this.colorState[style.colorMode] = this.getColorForValue(numericValue, style);
-  }
-
-  renderRowVariables(rowIndex: number) {
-    const scopedVars: any = {};
-    const row = this.rowGetter({ index: rowIndex });
-    for (let i = 0; i < row.length; i++) {
-      scopedVars[`__cell_${i}`] = { value: row[i] };
-    }
-    return scopedVars;
-  }
-
-  renderCell(columnIndex: number, rowIndex: number, value: any): ReactNode {
-    const column = this.columns[columnIndex];
-    if (column.formatter) {
-      value = column.formatter(value, column.style);
-    }
-
-    const style: CSSProperties = {};
-    const cellClasses = [];
-    let cellClass = '';
-
-    if (this.colorState.cell) {
-      style.backgroundColor = this.colorState.cell;
-      style.color = 'white';
-      this.colorState.cell = null;
-    } else if (this.colorState.value) {
-      style.color = this.colorState.value;
-      this.colorState.value = null;
-    }
-
-    if (value === undefined) {
-      style.display = 'none';
-      column.hidden = true;
-    } else {
-      column.hidden = false;
-    }
-
-    if (column.style && column.style.preserveFormat) {
-      cellClasses.push('table-panel-cell-pre');
-    }
-
-    let columnHtml: JSX.Element;
-    if (column.style && column.style.link) {
-      // Render cell as link
-      const { replaceVariables } = this.props;
-      const scopedVars = this.renderRowVariables(rowIndex);
-      scopedVars['__cell'] = { value: value };
-
-      const cellLink = replaceVariables(column.style.linkUrl, scopedVars, encodeURIComponent);
-      const cellLinkTooltip = replaceVariables(column.style.linkTooltip, scopedVars);
-      const cellTarget = column.style.linkTargetBlank ? '_blank' : '';
-
-      cellClasses.push('table-panel-cell-link');
-      columnHtml = (
-        <a
-          href={cellLink}
-          target={cellTarget}
-          data-link-tooltip
-          data-original-title={cellLinkTooltip}
-          data-placement="right"
-        >
-          {value}
-        </a>
-      );
-    } else {
-      columnHtml = <span>{value}</span>;
-    }
-
-    let filterLink: JSX.Element | null = null;
-    if (column.filterable) {
-      cellClasses.push('table-panel-cell-filterable');
-      filterLink = (
-        <span>
-          <a
-            className="table-panel-filter-link"
-            data-link-tooltip
-            data-original-title="Filter out value"
-            data-placement="bottom"
-            data-row={rowIndex}
-            data-column={columnIndex}
-            data-operator="!="
-          >
-            <i className="fa fa-search-minus" />
-          </a>
-          <a
-            className="table-panel-filter-link"
-            data-link-tooltip
-            data-original-title="Filter for value"
-            data-placement="bottom"
-            data-row={rowIndex}
-            data-column={columnIndex}
-            data-operator="="
-          >
-            <i className="fa fa-search-plus" />
-          </a>
-        </span>
-      );
-    }
-
-    if (cellClasses.length) {
-      cellClass = cellClasses.join(' ');
-    }
-
-    style.width = '100%';
-    style.height = '100%';
-    columnHtml = (
-      <div className={cellClass} style={style}>
-        {columnHtml}
-        {filterLink}
-      </div>
-    );
-    return columnHtml;
-  }
-
-  //----------------------------------------------------------------------
-  //----------------------------------------------------------------------
-
-  rowGetter = ({ index }: Index) => {
-    return this.state.data!.rows[index];
-  };
-
-  doSort = (info: any) => {
-    let dir = info.sortDirection;
-    let sort = info.sortBy;
-    if (sort !== this.state.sortBy) {
-      dir = 'DESC';
-    } else if (dir === 'DESC') {
-      dir = 'ASC';
-    } else {
-      sort = null;
-    }
-    this.setState({ sortBy: sort, sortDirection: dir });
-  };
-
-  headerRenderer = (header: TableHeaderProps): ReactNode => {
-    const dataKey = header.dataKey as any; // types say string, but it is number!
-    const { data, sortBy, sortDirection } = this.state;
-    const col = data!.columns[dataKey];
-
-    return (
-      <div>
-        {col.text} {sortBy === dataKey && <SortIndicator sortDirection={sortDirection} />}
-      </div>
-    );
-  };
-
-  cellRenderer = (cell: TableCellProps) => {
-    const { columnIndex, rowIndex } = cell;
-    const row = this.state.data!.rows[rowIndex];
-    const val = row[columnIndex];
-    return this.renderCell(columnIndex, rowIndex, val);
-  };
-
-  render() {
-    const { width, height, showHeader } = this.props;
-    const { data } = this.props;
-    if (!data) {
-      return <div>NO Data</div>;
-    }
-
-    return (
-      <RVTable
-        disableHeader={!showHeader}
-        headerHeight={30}
-        height={height}
-        overscanRowCount={10}
-        rowHeight={30}
-        rowGetter={this.rowGetter}
-        rowCount={data.rows.length}
-        sort={this.doSort}
-        width={width}
-      >
-        {data.columns.map((col, index) => {
-          return (
-            <RVColumn
-              key={index}
-              dataKey={index}
-              headerRenderer={this.headerRenderer}
-              cellRenderer={this.cellRenderer}
-              width={150}
-              minWidth={50}
-              flexGrow={1}
-            />
-          );
-        })}
-      </RVTable>
-    );
-  }
-}
-
-export default TableXXXX;

+ 1 - 0
packages/grafana-ui/src/components/Table/_Table.scss

@@ -59,6 +59,7 @@
   border-bottom: 2px solid $body-bg;
 
   cursor: pointer;
+  white-space: nowrap;
 
   color: $blue;
 }

+ 1 - 1
packages/grafana-ui/src/components/Table/examples.ts

@@ -1,5 +1,5 @@
 import { TableData } from '../../types/data';
-import { ColumnStyle } from './Table';
+import { ColumnStyle } from './TableCellBuilder';
 
 import { getColorDefinitionByName } from '@grafana/ui';
 

+ 1 - 0
packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import { AutoSizer } from 'react-virtualized';
 
+/** This will add full size with & height properties */
 export const withFullSizeStory = (component: React.ComponentType<any>, props: any) => (
   <div
     style={{

+ 1 - 1
public/app/plugins/panel/table/renderer.ts

@@ -2,7 +2,7 @@ import _ from 'lodash';
 import moment from 'moment';
 import kbn from 'app/core/utils/kbn';
 import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType } from '@grafana/ui';
-import { ColumnStyle } from '@grafana/ui/src/components/Table/Table';
+import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
 
 export class TableRenderer {
   formatters: any[];

+ 1 - 1
public/app/plugins/panel/table2/types.ts

@@ -1,4 +1,4 @@
-import { ColumnStyle } from '@grafana/ui/src/components/Table/Table';
+import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
 
 export interface Options {
   showHeader: boolean;