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

+ 351 - 42
public/app/plugins/panel/table2/TablePanel.tsx

@@ -1,67 +1,376 @@
 // Libraries
 import _ from 'lodash';
+import moment from 'moment';
 import React, { PureComponent } from 'react';
 
+import ReactTable from 'react-table';
+
+import { sanitize } from 'app/core/utils/text';
+
 // Types
 import { PanelProps } from '@grafana/ui/src/types';
-import { Options } from './types';
+import { Options, Style, Column, CellFormatter } from './types';
+import kbn from 'app/core/utils/kbn';
 
-import { Table, Index, Column } from 'react-virtualized';
+import templateSrv from 'app/features/templating/template_srv';
 
 interface Props extends PanelProps<Options> {}
 
 export class TablePanel extends PureComponent<Props> {
-  getRow = (index: Index): any => {
+  isUTC: false; // TODO? get UTC from props?
+
+  columns: Column[];
+  colorState: any;
+
+  initColumns() {
+    this.colorState = {};
+
+    const { panelData, options } = this.props;
+    if (!panelData.tableData) {
+      this.columns = [];
+      return;
+    }
+    const { styles } = options;
+
+    this.columns = panelData.tableData.columns.map((col, index) => {
+      let title = col.text;
+      let style: Style = null;
+
+      for (let i = 0; i < styles.length; i++) {
+        const s = styles[i];
+        const regex = 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(style, col),
+      };
+    });
+  }
+
+  getColorForValue(value: any, style: Style) {
+    if (!style.thresholds) {
+      return null;
+    }
+    for (let i = style.thresholds.length; i > 0; i--) {
+      if (value >= style.thresholds[i - 1]) {
+        return style.colors[i];
+      }
+    }
+    return _.first(style.colors);
+  }
+
+  defaultCellFormatter(v: any, style: Style): string {
+    if (v === null || v === void 0 || v === undefined) {
+      return '';
+    }
+
+    if (_.isArray(v)) {
+      v = v.join(', ');
+    }
+
+    if (style && style.sanitize) {
+      return sanitize(v);
+    } else {
+      return _.escape(v);
+    }
+  }
+
+  createColumnFormatter(style: Style, header: any): 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.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 = kbn.valueFormats[style.unit || header.unit];
+
+      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: Style) {
+    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);
+  }
+
+  renderRowconstiables(rowIndex) {
     const { panelData } = this.props;
-    if (panelData.tableData) {
-      return panelData.tableData.rows[index.index];
+
+    const scopedVars = {};
+    const row = panelData.tableData.rows[rowIndex];
+    for (let i = 0; i < row.length; i++) {
+      scopedVars[`__cell_${i}`] = { value: row[i] };
     }
-    return null;
-  };
+    return scopedVars;
+  }
 
-  render() {
-    const { panelData, width, height, options } = this.props;
-    const { showHeader } = options;
+  renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) {
+    const column = this.columns[columnIndex];
+    if (column.formatter) {
+      value = column.formatter(value, column.style);
+    }
 
-    const headerClassName = null;
-    const headerHeight = 30;
-    const rowHeight = 20;
+    const style = {};
+    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;
+    }
 
-    let rowCount = 0;
+    if (column.style && column.style.preserveFormat) {
+      cellClasses.push('table-panel-cell-pre');
+    }
+
+    let columnHtml;
+    if (column.style && column.style.link) {
+      // Render cell as link
+      const scopedconsts = this.renderRowconstiables(rowIndex);
+      scopedconsts['__cell'] = { value: value };
+
+      const cellLink = templateSrv.replace(column.style.linkUrl, scopedconsts, encodeURIComponent);
+      const cellLinkTooltip = templateSrv.replace(column.style.linkTooltip, scopedconsts);
+      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;
+    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;
+  }
+
+  render() {
+    const { panelData, height, options } = this.props;
+    const { pageSize } = options;
+
+    let rows = [];
+    let columns = [];
     if (panelData.tableData) {
-      rowCount = panelData.tableData.rows.length;
+      this.initColumns();
+      const fields = this.columns.map(c => {
+        return c.accessor;
+      });
+      rows = panelData.tableData.rows.map(row => {
+        return _.zipObject(fields, row);
+      });
+      columns = this.columns.map((c, columnIndex) => {
+        return {
+          Header: c.header,
+          accessor: c.accessor,
+          filterable: !!c.filterable,
+          Cell: row => {
+            return this.renderCell(columnIndex, row.index, row.value);
+          },
+        };
+      });
+      console.log(templateSrv);
+      console.log(rows);
     } else {
       return <div>No Table Data...</div>;
     }
 
+    // Only show paging if necessary
+    const showPaginationBottom = pageSize && pageSize < panelData.tableData.rows.length;
+
     return (
-      <div>
-        <Table
-          disableHeader={!showHeader}
-          headerClassName={headerClassName}
-          headerHeight={headerHeight}
-          height={height}
-          overscanRowCount={5}
-          rowHeight={rowHeight}
-          rowGetter={this.getRow}
-          rowCount={rowCount}
-          width={width}
-        >
-          {panelData.tableData.columns.map((col, index) => {
-            return (
-              <Column
-                label={col.text}
-                cellDataGetter={({ rowData }) => {
-                  return rowData[index];
-                }}
-                dataKey={index}
-                disableSort={true}
-                width={100}
-              />
-            );
-          })}
-        </Table>
-      </div>
+      <ReactTable
+        data={rows}
+        columns={columns}
+        defaultPageSize={pageSize}
+        style={{
+          height: height - 20 + 'px',
+        }}
+        showPaginationBottom={showPaginationBottom}
+        getTdProps={(state, rowInfo, column, instance) => {
+          return {
+            onClick: (e, handleOriginal) => {
+              console.log('filter', rowInfo.row[column.id]);
+              if (handleOriginal) {
+                handleOriginal();
+              }
+            },
+          };
+        }}
+      />
     );
   }
 }

+ 9 - 2
public/app/plugins/panel/table2/TablePanelEditor.tsx

@@ -3,7 +3,7 @@ import _ from 'lodash';
 import React, { PureComponent } from 'react';
 
 // Types
-import { PanelEditorProps, Switch } from '@grafana/ui';
+import { PanelEditorProps, Switch, FormField } from '@grafana/ui';
 import { Options } from './types';
 
 export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
@@ -11,8 +11,10 @@ export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
     this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader });
   };
 
+  onRowsPerPageChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, pageSize: target.value });
+
   render() {
-    const { showHeader } = this.props.options;
+    const { showHeader, pageSize } = this.props.options;
 
     return (
       <div>
@@ -20,6 +22,11 @@ export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> {
           <h5 className="section-heading">Header</h5>
           <Switch label="Show" labelClass="width-5" checked={showHeader} onChange={this.onToggleShowHeader} />
         </div>
+
+        <div className="section gf-form-group">
+          <h5 className="section-heading">Paging</h5>
+          <FormField label="Rows per page" labelWidth={8} onChange={this.onRowsPerPageChange} value={pageSize} />
+        </div>
       </div>
     );
   }

+ 56 - 0
public/app/plugins/panel/table2/types.ts

@@ -1,7 +1,63 @@
+// Made to match the existing (untyped) settings in the angular table
+export interface Style {
+  alias?: string;
+  colorMode?: string;
+  colors?: any[];
+  decimals?: number;
+  pattern?: string;
+  thresholds?: any[];
+  type?: 'date' | 'number' | 'string' | 'hidden';
+  unit?: string;
+  dateFormat?: string;
+  sanitize?: boolean;
+  mappingType?: any;
+  valueMaps?: any;
+  rangeMaps?: any;
+
+  link?: any;
+  linkUrl?: any;
+  linkTooltip?: any;
+  linkTargetBlank?: boolean;
+
+  preserveFormat?: boolean;
+}
+
+export type CellFormatter = (v: any, style: Style) => string;
+
+export interface Column {
+  header: string;
+  accessor: string; // the field name
+  style?: Style;
+  hidden?: boolean;
+  formatter: CellFormatter;
+  filterable?: boolean;
+}
+
 export interface Options {
   showHeader: boolean;
+  styles: Style[]; // TODO, just a copy from existing table
+  pageSize: number;
 }
 
 export const defaults: Options = {
   showHeader: true,
+  styles: [
+    {
+      type: 'date',
+      pattern: 'Time',
+      alias: 'Time',
+      dateFormat: 'YYYY-MM-DD HH:mm:ss',
+    },
+    {
+      unit: 'short',
+      type: 'number',
+      alias: '',
+      decimals: 2,
+      colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
+      colorMode: null,
+      pattern: '/.*/',
+      thresholds: [],
+    },
+  ],
+  pageSize: 100,
 };

+ 0 - 1
public/sass/_grafana.scss

@@ -98,7 +98,6 @@
 @import 'components/page_loader';
 @import 'components/toggle_button_group';
 @import 'components/popover-box';
-@import 'components/react_virtualized';
 
 // LOAD @grafana/ui components
 @import '../../packages/grafana-ui/src/index';

+ 0 - 83
public/sass/components/_react_virtualized.scss

@@ -1,83 +0,0 @@
-/**
-COPIED FROM:
-https://raw.githubusercontent.com/bvaughn/react-virtualized/master/source/styles.css
-*/
-
-/* Collection default theme */
-
-.ReactVirtualized__Collection {
-}
-
-.ReactVirtualized__Collection__innerScrollContainer {
-}
-
-/* Grid default theme */
-
-.ReactVirtualized__Grid {
-}
-
-.ReactVirtualized__Grid__innerScrollContainer {
-}
-
-/* Table default theme */
-
-.ReactVirtualized__Table {
-}
-
-.ReactVirtualized__Table__Grid {
-}
-
-.ReactVirtualized__Table__headerRow {
-  font-weight: 700;
-  text-transform: uppercase;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-.ReactVirtualized__Table__row {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-
-.ReactVirtualized__Table__headerTruncatedText {
-  display: inline-block;
-  max-width: 100%;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  overflow: hidden;
-}
-
-.ReactVirtualized__Table__headerColumn,
-.ReactVirtualized__Table__rowColumn {
-  margin-right: 10px;
-  min-width: 0px;
-}
-.ReactVirtualized__Table__rowColumn {
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.ReactVirtualized__Table__headerColumn:first-of-type,
-.ReactVirtualized__Table__rowColumn:first-of-type {
-  margin-left: 10px;
-}
-.ReactVirtualized__Table__sortableHeaderColumn {
-  cursor: pointer;
-}
-
-.ReactVirtualized__Table__sortableHeaderIconContainer {
-  display: flex;
-  align-items: center;
-}
-.ReactVirtualized__Table__sortableHeaderIcon {
-  flex: 0 0 24px;
-  height: 1em;
-  width: 1em;
-  fill: currentColor;
-}
-
-/* List default theme */
-
-.ReactVirtualized__List {
-}