From f59ccdf59c37826371cca3e05a08181eda8d1151 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 5 Mar 2019 16:07:46 -0800 Subject: [PATCH 01/31] add a basic alpha react table --- .../app/features/plugins/built_in_plugins.ts | 2 + public/app/plugins/panel/table2/README.md | 9 ++ .../app/plugins/panel/table2/TablePanel.tsx | 67 +++++++++++++++ .../plugins/panel/table2/TablePanelEditor.tsx | 26 ++++++ .../panel/table2/img/icn-table-panel.svg | 67 +++++++++++++++ public/app/plugins/panel/table2/module.tsx | 9 ++ public/app/plugins/panel/table2/plugin.json | 19 +++++ public/app/plugins/panel/table2/types.ts | 7 ++ public/sass/_grafana.scss | 1 + .../sass/components/_react_virtualized.scss | 83 +++++++++++++++++++ 10 files changed, 290 insertions(+) create mode 100644 public/app/plugins/panel/table2/README.md create mode 100644 public/app/plugins/panel/table2/TablePanel.tsx create mode 100644 public/app/plugins/panel/table2/TablePanelEditor.tsx create mode 100644 public/app/plugins/panel/table2/img/icn-table-panel.svg create mode 100644 public/app/plugins/panel/table2/module.tsx create mode 100644 public/app/plugins/panel/table2/plugin.json create mode 100644 public/app/plugins/panel/table2/types.ts create mode 100644 public/sass/components/_react_virtualized.scss diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 078443b019a..dba6478baab 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -23,6 +23,7 @@ import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module'; import * as alertListPanel from 'app/plugins/panel/alertlist/module'; import * as heatmapPanel from 'app/plugins/panel/heatmap/module'; import * as tablePanel from 'app/plugins/panel/table/module'; +import * as table2Panel from 'app/plugins/panel/table2/module'; import * as singlestatPanel from 'app/plugins/panel/singlestat/module'; import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module'; import * as gaugePanel from 'app/plugins/panel/gauge/module'; @@ -53,6 +54,7 @@ const builtInPlugins = { 'app/plugins/panel/alertlist/module': alertListPanel, 'app/plugins/panel/heatmap/module': heatmapPanel, 'app/plugins/panel/table/module': tablePanel, + 'app/plugins/panel/table2/module': table2Panel, 'app/plugins/panel/singlestat/module': singlestatPanel, 'app/plugins/panel/gettingstarted/module': gettingStartedPanel, 'app/plugins/panel/gauge/module': gaugePanel, diff --git a/public/app/plugins/panel/table2/README.md b/public/app/plugins/panel/table2/README.md new file mode 100644 index 00000000000..98f2c13f75c --- /dev/null +++ b/public/app/plugins/panel/table2/README.md @@ -0,0 +1,9 @@ +# Table Panel - Native Plugin + +The Table Panel is **included** with Grafana. + +The table panel is very flexible, supporting both multiple modes for time series as well as for table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options. + +Check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase) or read more about it here: + +[http://docs.grafana.org/reference/table_panel/](http://docs.grafana.org/reference/table_panel/) diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx new file mode 100644 index 00000000000..d18d0f7cd36 --- /dev/null +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -0,0 +1,67 @@ +// Libraries +import _ from 'lodash'; +import React, { PureComponent } from 'react'; + +// Types +import { PanelProps } from '@grafana/ui/src/types'; +import { Options } from './types'; + +import { Table, Index, Column } from 'react-virtualized'; + +interface Props extends PanelProps {} + +export class TablePanel extends PureComponent { + getRow = (index: Index): any => { + const { panelData } = this.props; + if (panelData.tableData) { + return panelData.tableData.rows[index.index]; + } + return null; + }; + + render() { + const { panelData, width, height, options } = this.props; + const { showHeader } = options; + + const headerClassName = null; + const headerHeight = 30; + const rowHeight = 20; + + let rowCount = 0; + if (panelData.tableData) { + rowCount = panelData.tableData.rows.length; + } else { + return
No Table Data...
; + } + + return ( +
+ + {panelData.tableData.columns.map((col, index) => { + return ( + { + return rowData[index]; + }} + dataKey={index} + disableSort={true} + width={100} + /> + ); + })} +
+
+ ); + } +} diff --git a/public/app/plugins/panel/table2/TablePanelEditor.tsx b/public/app/plugins/panel/table2/TablePanelEditor.tsx new file mode 100644 index 00000000000..60d2eff9b85 --- /dev/null +++ b/public/app/plugins/panel/table2/TablePanelEditor.tsx @@ -0,0 +1,26 @@ +//// Libraries +import _ from 'lodash'; +import React, { PureComponent } from 'react'; + +// Types +import { PanelEditorProps, Switch } from '@grafana/ui'; +import { Options } from './types'; + +export class TablePanelEditor extends PureComponent> { + onToggleShowHeader = () => { + this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader }); + }; + + render() { + const { showHeader } = this.props.options; + + return ( +
+
+
Header
+ +
+
+ ); + } +} diff --git a/public/app/plugins/panel/table2/img/icn-table-panel.svg b/public/app/plugins/panel/table2/img/icn-table-panel.svg new file mode 100644 index 00000000000..83097e259dc --- /dev/null +++ b/public/app/plugins/panel/table2/img/icn-table-panel.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/table2/module.tsx b/public/app/plugins/panel/table2/module.tsx new file mode 100644 index 00000000000..d93e7911074 --- /dev/null +++ b/public/app/plugins/panel/table2/module.tsx @@ -0,0 +1,9 @@ +import { ReactPanelPlugin } from '@grafana/ui'; + +import { TablePanelEditor } from './TablePanelEditor'; +import { TablePanel } from './TablePanel'; +import { Options, defaults } from './types'; + +export const reactPanel = new ReactPanelPlugin(TablePanel); +reactPanel.setEditor(TablePanelEditor); +reactPanel.setDefaults(defaults); diff --git a/public/app/plugins/panel/table2/plugin.json b/public/app/plugins/panel/table2/plugin.json new file mode 100644 index 00000000000..4fa7728bd55 --- /dev/null +++ b/public/app/plugins/panel/table2/plugin.json @@ -0,0 +1,19 @@ +{ + "type": "panel", + "name": "React Table", + "id": "table2", + "state": "alpha", + + "dataFormats": ["table"], + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-table-panel.svg", + "large": "img/icn-table-panel.svg" + } + } +} diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts new file mode 100644 index 00000000000..6814ce6a60c --- /dev/null +++ b/public/app/plugins/panel/table2/types.ts @@ -0,0 +1,7 @@ +export interface Options { + showHeader: boolean; +} + +export const defaults: Options = { + showHeader: true, +}; diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 8928523f2be..7699947fae2 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -98,6 +98,7 @@ @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'; diff --git a/public/sass/components/_react_virtualized.scss b/public/sass/components/_react_virtualized.scss new file mode 100644 index 00000000000..9c65f035df0 --- /dev/null +++ b/public/sass/components/_react_virtualized.scss @@ -0,0 +1,83 @@ +/** +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 { +} From 372e892fab1119f5a39875827c078ef627ff01bc Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 5 Mar 2019 18:17:44 -0800 Subject: [PATCH 02/31] use react-table --- .../app/plugins/panel/table2/TablePanel.tsx | 395 ++++++++++++++++-- .../plugins/panel/table2/TablePanelEditor.tsx | 11 +- public/app/plugins/panel/table2/types.ts | 56 +++ public/sass/_grafana.scss | 1 - .../sass/components/_react_virtualized.scss | 83 ---- 5 files changed, 417 insertions(+), 129 deletions(-) delete mode 100644 public/sass/components/_react_virtualized.scss diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index d18d0f7cd36..eba47728bdc 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/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 {} export class TablePanel extends PureComponent { - getRow = (index: Index): any => { - const { panelData } = this.props; - if (panelData.tableData) { - return panelData.tableData.rows[index.index]; + 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; } - return null; - }; + 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; + + const scopedVars = {}; + const row = panelData.tableData.rows[rowIndex]; + for (let i = 0; i < row.length; i++) { + scopedVars[`__cell_${i}`] = { value: row[i] }; + } + return scopedVars; + } + + renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) { + const column = this.columns[columnIndex]; + if (column.formatter) { + value = column.formatter(value, column.style); + } + + 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; + } + + 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 = ( + + {value} + + ); + } else { + columnHtml = {value}; + } + + let filterLink; + if (column.filterable) { + cellClasses.push('table-panel-cell-filterable'); + filterLink = ( + + + + + + + + + ); + } + + if (cellClasses.length) { + cellClass = cellClasses.join(' '); + } + + style['width'] = '100%'; + style['height'] = '100%'; + columnHtml = ( +
+ {columnHtml} + {filterLink} +
+ ); + return columnHtml; + } render() { - const { panelData, width, height, options } = this.props; - const { showHeader } = options; + const { panelData, height, options } = this.props; + const { pageSize } = options; - const headerClassName = null; - const headerHeight = 30; - const rowHeight = 20; - - let rowCount = 0; + 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
No Table Data...
; } + // Only show paging if necessary + const showPaginationBottom = pageSize && pageSize < panelData.tableData.rows.length; + return ( -
- - {panelData.tableData.columns.map((col, index) => { - return ( - { - return rowData[index]; - }} - dataKey={index} - disableSort={true} - width={100} - /> - ); - })} -
-
+ { + return { + onClick: (e, handleOriginal) => { + console.log('filter', rowInfo.row[column.id]); + if (handleOriginal) { + handleOriginal(); + } + }, + }; + }} + /> ); } } diff --git a/public/app/plugins/panel/table2/TablePanelEditor.tsx b/public/app/plugins/panel/table2/TablePanelEditor.tsx index 60d2eff9b85..fc899bd22d2 100644 --- a/public/app/plugins/panel/table2/TablePanelEditor.tsx +++ b/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> { @@ -11,8 +11,10 @@ export class TablePanelEditor extends PureComponent> { 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 (
@@ -20,6 +22,11 @@ export class TablePanelEditor extends PureComponent> {
Header
+ +
+
Paging
+ +
); } diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index 6814ce6a60c..3251c97a432 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/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, }; diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 7699947fae2..8928523f2be 100644 --- a/public/sass/_grafana.scss +++ b/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'; diff --git a/public/sass/components/_react_virtualized.scss b/public/sass/components/_react_virtualized.scss deleted file mode 100644 index 9c65f035df0..00000000000 --- a/public/sass/components/_react_virtualized.scss +++ /dev/null @@ -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 { -} From 32543cac1051a03c7e3a54e1a016798048cc3860 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 5 Mar 2019 18:23:13 -0800 Subject: [PATCH 03/31] use typescrit in angular table --- public/app/plugins/panel/table/renderer.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index e9bf89f45fe..b77bea5bd46 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import moment from 'moment'; import kbn from 'app/core/utils/kbn'; import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType } from '@grafana/ui'; +import { Style } from '../table2/types'; export class TableRenderer { formatters: any[]; @@ -51,7 +52,7 @@ export class TableRenderer { } } - getColorForValue(value, style) { + getColorForValue(value, style: Style) { if (!style.thresholds) { return null; } @@ -63,7 +64,7 @@ export class TableRenderer { return getColorFromHexRgbOrName(_.first(style.colors), this.theme); } - defaultCellFormatter(v, style) { + defaultCellFormatter(v, style: Style) { if (v === null || v === void 0 || v === undefined) { return ''; } @@ -190,7 +191,7 @@ export class TableRenderer { }; } - setColorState(value, style) { + setColorState(value, style: Style) { if (!style.colorMode) { return; } From 589229d8afb718a94d1475c420ba92e0600ffbc5 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 5 Mar 2019 18:55:09 -0800 Subject: [PATCH 04/31] fix variable name --- public/app/plugins/panel/table2/TablePanel.tsx | 4 ++-- public/app/plugins/panel/table2/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index eba47728bdc..c9669299125 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -205,7 +205,7 @@ export class TablePanel extends PureComponent { this.colorState[style.colorMode] = this.getColorForValue(numericValue, style); } - renderRowconstiables(rowIndex) { + renderRowVariables(rowIndex) { const { panelData } = this.props; const scopedVars = {}; @@ -249,7 +249,7 @@ export class TablePanel extends PureComponent { let columnHtml; if (column.style && column.style.link) { // Render cell as link - const scopedconsts = this.renderRowconstiables(rowIndex); + const scopedconsts = this.renderRowVariables(rowIndex); scopedconsts['__cell'] = { value: value }; const cellLink = templateSrv.replace(column.style.linkUrl, scopedconsts, encodeURIComponent); diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index 3251c97a432..a06d833107d 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/public/app/plugins/panel/table2/types.ts @@ -35,7 +35,7 @@ export interface Column { export interface Options { showHeader: boolean; - styles: Style[]; // TODO, just a copy from existing table + styles: Style[]; pageSize: number; } From f3712f748a159e9ba5e9fd53a0495326bfe4549e Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 5 Mar 2019 20:11:14 -0800 Subject: [PATCH 05/31] add test file (ignored) --- .../app/plugins/panel/table2/TablePanel.tsx | 28 +- .../panel/table2/specs/renderer.test.ts | 408 ++++++++++++++++++ 2 files changed, 422 insertions(+), 14 deletions(-) create mode 100644 public/app/plugins/panel/table2/specs/renderer.test.ts diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index c9669299125..2d5ef4bd695 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -1,7 +1,7 @@ // Libraries import _ from 'lodash'; import moment from 'moment'; -import React, { PureComponent } from 'react'; +import React, { PureComponent, CSSProperties } from 'react'; import ReactTable from 'react-table'; @@ -222,21 +222,21 @@ export class TablePanel extends PureComponent { value = column.formatter(value, column.style); } - const style = {}; + const style: CSSProperties = {}; const cellClasses = []; let cellClass = ''; if (this.colorState.cell) { - style['backgroundColor'] = this.colorState.cell; - style['color'] = 'white'; + style.backgroundColor = this.colorState.cell; + style.color = 'white'; this.colorState.cell = null; } else if (this.colorState.value) { - style['color'] = this.colorState.value; + style.color = this.colorState.value; this.colorState.value = null; } if (value === undefined) { - style['display'] = 'none'; + style.display = 'none'; column.hidden = true; } else { column.hidden = false; @@ -246,14 +246,14 @@ export class TablePanel extends PureComponent { cellClasses.push('table-panel-cell-pre'); } - let columnHtml; + let columnHtml: JSX.Element; if (column.style && column.style.link) { // Render cell as link - const scopedconsts = this.renderRowVariables(rowIndex); - scopedconsts['__cell'] = { value: value }; + const scopedVars = this.renderRowVariables(rowIndex); + scopedVars['__cell'] = { value: value }; - const cellLink = templateSrv.replace(column.style.linkUrl, scopedconsts, encodeURIComponent); - const cellLinkTooltip = templateSrv.replace(column.style.linkTooltip, scopedconsts); + const cellLink = templateSrv.replace(column.style.linkUrl, scopedVars, encodeURIComponent); + const cellLinkTooltip = templateSrv.replace(column.style.linkTooltip, scopedVars); const cellTarget = column.style.linkTargetBlank ? '_blank' : ''; cellClasses.push('table-panel-cell-link'); @@ -272,7 +272,7 @@ export class TablePanel extends PureComponent { columnHtml = {value}; } - let filterLink; + let filterLink: JSX.Element; if (column.filterable) { cellClasses.push('table-panel-cell-filterable'); filterLink = ( @@ -307,8 +307,8 @@ export class TablePanel extends PureComponent { cellClass = cellClasses.join(' '); } - style['width'] = '100%'; - style['height'] = '100%'; + style.width = '100%'; + style.height = '100%'; columnHtml = (
{columnHtml} diff --git a/public/app/plugins/panel/table2/specs/renderer.test.ts b/public/app/plugins/panel/table2/specs/renderer.test.ts new file mode 100644 index 00000000000..c76ccc8f716 --- /dev/null +++ b/public/app/plugins/panel/table2/specs/renderer.test.ts @@ -0,0 +1,408 @@ +import _ from 'lodash'; +import TableModel from 'app/core/table_model'; +import { TablePanel } from '../TablePanel'; +import { getColorDefinitionByName } from '@grafana/ui'; +import { Options } from '../types'; +import { PanelProps, LoadingState } from '@grafana/ui/src/types'; +import moment from 'moment'; + +// TODO: this is commented out with *x* describe! +// Essentially all the elements need to replace the with
+xdescribe('when rendering table', () => { + const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange'); + + describe('given 13 columns', () => { + const table = new TableModel(); + table.columns = [ + { text: 'Time' }, + { text: 'Value' }, + { text: 'Colored' }, + { text: 'Undefined' }, + { text: 'String' }, + { text: 'United', unit: 'bps' }, + { text: 'Sanitized' }, + { text: 'Link' }, + { text: 'Array' }, + { text: 'Mapping' }, + { text: 'RangeMapping' }, + { text: 'MappingColored' }, + { text: 'RangeMappingColored' }, + ]; + table.rows = [ + [1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2], + ]; + + const panel: Options = { + showHeader: true, + pageSize: 10, + styles: [ + { + pattern: 'Time', + type: 'date', + alias: 'Timestamp', + }, + { + pattern: '/(Val)ue/', + type: 'number', + unit: 'ms', + decimals: 3, + alias: '$1', + }, + { + pattern: 'Colored', + type: 'number', + unit: 'none', + decimals: 1, + colorMode: 'value', + thresholds: [50, 80], + colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], + }, + { + pattern: 'String', + type: 'string', + }, + { + pattern: 'String', + type: 'string', + }, + { + pattern: 'United', + type: 'number', + unit: 'ms', + decimals: 2, + }, + { + pattern: 'Sanitized', + type: 'string', + sanitize: true, + }, + { + pattern: 'Link', + type: 'string', + link: true, + linkUrl: '/dashboard?param=$__cell¶m_1=$__cell_1¶m_2=$__cell_2', + linkTooltip: '$__cell $__cell_1 $__cell_6', + linkTargetBlank: true, + }, + { + pattern: 'Array', + type: 'number', + unit: 'ms', + decimals: 3, + }, + { + pattern: 'Mapping', + type: 'string', + mappingType: 1, + valueMaps: [ + { + value: '1', + text: 'on', + }, + { + value: '0', + text: 'off', + }, + { + value: 'HELLO WORLD', + text: 'HELLO GRAFANA', + }, + { + value: 'value1, value2', + text: 'value3, value4', + }, + ], + }, + { + pattern: 'RangeMapping', + type: 'string', + mappingType: 2, + rangeMaps: [ + { + from: '1', + to: '3', + text: 'on', + }, + { + from: '3', + to: '6', + text: 'off', + }, + ], + }, + { + pattern: 'MappingColored', + type: 'string', + mappingType: 1, + valueMaps: [ + { + value: '1', + text: 'on', + }, + { + value: '0', + text: 'off', + }, + ], + colorMode: 'value', + thresholds: [1, 2], + colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], + }, + { + pattern: 'RangeMappingColored', + type: 'string', + mappingType: 2, + rangeMaps: [ + { + from: '1', + to: '3', + text: 'on', + }, + { + from: '3', + to: '6', + text: 'off', + }, + ], + colorMode: 'value', + thresholds: [2, 5], + colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], + }, + ], + }; + + // const sanitize = value => { + // return 'sanitized'; + // }; + + const props: PanelProps = { + panelData: { + tableData: table, + }, + width: 100, + height: 100, + timeRange: { + from: moment(), + to: moment(), + raw: { + from: moment(), + to: moment(), + }, + }, + loading: LoadingState.Done, + replaceVariables: (value, scopedVars) => { + if (scopedVars) { + // For testing variables replacement in link + _.each(scopedVars, (val, key) => { + value = value.replace('$' + key, val.value); + }); + } + return value; + }, + renderCounter: 1, + options: panel, + }; + + const renderer = new TablePanel(props); //panel, table, 'utc', sanitize, templateSrv); + renderer.render(); // This will initalize + + it('time column should be formated', () => { + const html = renderer.renderCell(0, 0, 1388556366666); + expect(html).toBe('2014-01-01T06:06:06Z'); + }); + + it('time column with epoch as string should be formatted', () => { + const html = renderer.renderCell(0, 0, '1388556366666'); + expect(html).toBe('2014-01-01T06:06:06Z'); + }); + + 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('2018-12-01T01:00:00Z'); + }); + + 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('2018-12-01T01:00:00Z'); + }); + + it('undefined time column should be rendered as -', () => { + const html = renderer.renderCell(0, 0, undefined); + expect(html).toBe('-'); + }); + + it('null time column should be rendered as -', () => { + const html = renderer.renderCell(0, 0, null); + expect(html).toBe('-'); + }); + + it('number column with unit specified should ignore style unit', () => { + const html = renderer.renderCell(5, 0, 1230); + expect(html).toBe('1.23 kbps'); + }); + + it('number column should be formated', () => { + const html = renderer.renderCell(1, 0, 1230); + expect(html).toBe('1.230 s'); + }); + + it('number style should ignore string values', () => { + const html = renderer.renderCell(1, 0, 'asd'); + expect(html).toBe('asd'); + }); + + it('colored cell should have style (handles HEX color values)', () => { + const html = renderer.renderCell(2, 0, 40); + expect(html).toBe('40.0'); + }); + + it('colored cell should have style (handles named color values', () => { + const html = renderer.renderCell(2, 0, 55); + expect(html).toBe(`55.0`); + }); + + it('colored cell should have style handles(rgb color values)', () => { + const html = renderer.renderCell(2, 0, 85); + expect(html).toBe('85.0'); + }); + + it('unformated undefined should be rendered as string', () => { + const html = renderer.renderCell(3, 0, 'value'); + expect(html).toBe('value'); + }); + + it('string style with escape html should return escaped html', () => { + const html = renderer.renderCell(4, 0, '&breaking
the
row'); + expect(html).toBe('&breaking <br /> the <br /> row'); + }); + + it('undefined formater should return escaped html', () => { + const html = renderer.renderCell(3, 0, '&breaking
the
row'); + expect(html).toBe('&breaking <br /> the <br /> row'); + }); + + it('undefined value should render as -', () => { + const html = renderer.renderCell(3, 0, undefined); + expect(html).toBe(''); + }); + + it('sanitized value should render as', () => { + const html = renderer.renderCell(6, 0, 'text link'); + expect(html).toBe('sanitized'); + }); + + 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 = ` + + + host1 + + + `; + 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('value1, value2'); + }); + + it('numeric value should be mapped to text', () => { + const html = renderer.renderCell(9, 0, 1); + expect(html).toBe('on'); + }); + + it('string numeric value should be mapped to text', () => { + const html = renderer.renderCell(9, 0, '0'); + expect(html).toBe('off'); + }); + + it('string value should be mapped to text', () => { + const html = renderer.renderCell(9, 0, 'HELLO WORLD'); + expect(html).toBe('HELLO GRAFANA'); + }); + + it('array column value should be mapped to text', () => { + const html = renderer.renderCell(9, 0, ['value1', 'value2']); + expect(html).toBe('value3, value4'); + }); + + it('value should be mapped to text (range)', () => { + const html = renderer.renderCell(10, 0, 2); + expect(html).toBe('on'); + }); + + it('value should be mapped to text (range)', () => { + const html = renderer.renderCell(10, 0, 5); + expect(html).toBe('off'); + }); + + it('array column value should not be mapped to text', () => { + const html = renderer.renderCell(10, 0, ['value1', 'value2']); + expect(html).toBe('value1, value2'); + }); + + it('value should be mapped to text and colored cell should have style', () => { + const html = renderer.renderCell(11, 0, 1); + expect(html).toBe(`on`); + }); + + it('value should be mapped to text and colored cell should have style', () => { + const html = renderer.renderCell(11, 0, '1'); + expect(html).toBe(`on`); + }); + + it('value should be mapped to text and colored cell should have style', () => { + const html = renderer.renderCell(11, 0, 0); + expect(html).toBe('off'); + }); + + it('value should be mapped to text and colored cell should have style', () => { + const html = renderer.renderCell(11, 0, '0'); + expect(html).toBe('off'); + }); + + it('value should be mapped to text and colored cell should have style', () => { + const html = renderer.renderCell(11, 0, '2.1'); + expect(html).toBe('2.1'); + }); + + it('value should be mapped to text (range) and colored cell should have style', () => { + const html = renderer.renderCell(12, 0, 0); + expect(html).toBe('0'); + }); + + it('value should be mapped to text (range) and colored cell should have style', () => { + const html = renderer.renderCell(12, 0, 1); + expect(html).toBe('on'); + }); + + it('value should be mapped to text (range) and colored cell should have style', () => { + const html = renderer.renderCell(12, 0, 4); + expect(html).toBe(`off`); + }); + + 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('7.1'); + }); + }); +}); + +function normalize(str) { + return str.replace(/\s+/gm, ' ').trim(); +} From 8cb52ee6265eca5ea145f77b998ef7ac490595bc Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 5 Mar 2019 20:40:34 -0800 Subject: [PATCH 06/31] set height --- public/app/plugins/panel/table2/TablePanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index 2d5ef4bd695..76737b672dd 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -355,9 +355,9 @@ export class TablePanel extends PureComponent { { From 123739fdb457fe54fd1c84ab52247f5dabcddf09 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 6 Mar 2019 01:14:53 -0800 Subject: [PATCH 07/31] use props.replaceVariables rather than templateSrv --- public/app/plugins/panel/table2/TablePanel.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index 76737b672dd..691d9f988e6 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -12,8 +12,6 @@ import { PanelProps } from '@grafana/ui/src/types'; import { Options, Style, Column, CellFormatter } from './types'; import kbn from 'app/core/utils/kbn'; -import templateSrv from 'app/features/templating/template_srv'; - interface Props extends PanelProps {} export class TablePanel extends PureComponent { @@ -252,8 +250,10 @@ export class TablePanel extends PureComponent { const scopedVars = this.renderRowVariables(rowIndex); scopedVars['__cell'] = { value: value }; - const cellLink = templateSrv.replace(column.style.linkUrl, scopedVars, encodeURIComponent); - const cellLinkTooltip = templateSrv.replace(column.style.linkTooltip, scopedVars); + const { replaceVariables } = this.props; + + 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'); @@ -342,8 +342,6 @@ export class TablePanel extends PureComponent { }, }; }); - console.log(templateSrv); - console.log(rows); } else { return
No Table Data...
; } From aca69df755ceda0d72cc5acfc9f0dbaf607bce6f Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 6 Mar 2019 10:10:04 -0800 Subject: [PATCH 08/31] try virtualized --- .../app/plugins/panel/table2/TablePanel.tsx | 137 +++++++++++------- public/app/plugins/panel/table2/types.ts | 2 +- public/sass/_grafana.scss | 1 + public/sass/components/_panel_table2.scss | 64 ++++++++ 4 files changed, 151 insertions(+), 53 deletions(-) create mode 100644 public/sass/components/_panel_table2.scss diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index 691d9f988e6..1cd00ec6b74 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -1,25 +1,39 @@ // Libraries import _ from 'lodash'; import moment from 'moment'; -import React, { PureComponent, CSSProperties } from 'react'; - -import ReactTable from 'react-table'; +import React, { Component, CSSProperties, ReactNode } from 'react'; import { sanitize } from 'app/core/utils/text'; // Types import { PanelProps } from '@grafana/ui/src/types'; -import { Options, Style, Column, CellFormatter } from './types'; +import { Options, Style, CellFormatter, ColumnInfo } from './types'; import kbn from 'app/core/utils/kbn'; +import { Table, SortDirectionType, SortIndicator, Column, TableHeaderProps, TableCellProps } from 'react-virtualized'; interface Props extends PanelProps {} -export class TablePanel extends PureComponent { +interface State { + sortBy: string; + sortDirection: SortDirectionType; + sortedList: any[]; +} + +export class TablePanel extends Component { isUTC: false; // TODO? get UTC from props? - columns: Column[]; + columns: ColumnInfo[]; colorState: any; + constructor(props: Props) { + super(props); + this.state = { + sortBy: 'index', + sortDirection: 'ASC', + sortedList: [], + }; + } + initColumns() { this.colorState = {}; @@ -318,57 +332,76 @@ export class TablePanel extends PureComponent { return columnHtml; } - render() { - const { panelData, height, options } = this.props; - const { pageSize } = options; + _rowGetter = ({ index }) => { + return this.props.panelData.tableData.rows[index]; + }; - let rows = []; - let columns = []; - if (panelData.tableData) { - 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); - }, - }; - }); - } else { + _sort = ({ sortBy, sortDirection }) => { + // const sortedList = this._sortList({sortBy, sortDirection}); + + // this.setState({sortBy, sortDirection, sortedList}); + console.log('TODO, sort!', sortBy, sortDirection); + }; + + _headerRenderer = (header: TableHeaderProps): ReactNode => { + const tableData = this.props.panelData.tableData!; + const col = tableData.columns[header.dataKey]; + if (!col) { + return
??{header.dataKey}
; + } + + return ( +
+ {col.text} {header.sortBy === header.dataKey && } +
+ ); + }; + + _cellRenderer = (cell: TableCellProps) => { + const tableData = this.props.panelData.tableData!; + const val = tableData.rows[cell.rowIndex][cell.dataKey]; + return
{val}
; + }; + + render() { + const { panelData, width, height, options } = this.props; + const { showHeader } = options; + const { sortBy, sortDirection } = this.state; + const { tableData } = panelData; + + if (!tableData) { return
No Table Data...
; } - // Only show paging if necessary - const showPaginationBottom = pageSize && pageSize < panelData.tableData.rows.length; - return ( - { - return { - onClick: (e, handleOriginal) => { - console.log('filter', rowInfo.row[column.id]); - if (handleOriginal) { - handleOriginal(); - } - }, - }; - }} - /> + + {tableData.columns.map((col, index) => { + return ( + + ); + })} +
); } } diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index a06d833107d..6ff384b06e0 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/public/app/plugins/panel/table2/types.ts @@ -24,7 +24,7 @@ export interface Style { export type CellFormatter = (v: any, style: Style) => string; -export interface Column { +export interface ColumnInfo { header: string; accessor: string; // the field name style?: Style; diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 8928523f2be..5104feac48e 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -57,6 +57,7 @@ @import 'components/panel_pluginlist'; @import 'components/panel_singlestat'; @import 'components/panel_table'; +@import 'components/panel_table2'; @import 'components/panel_text'; @import 'components/panel_heatmap'; @import 'components/panel_logs'; diff --git a/public/sass/components/_panel_table2.scss b/public/sass/components/_panel_table2.scss new file mode 100644 index 00000000000..891588a8687 --- /dev/null +++ b/public/sass/components/_panel_table2.scss @@ -0,0 +1,64 @@ +.ReactVirtualized__Table { +} + +.ReactVirtualized__Table__Grid { +} + +.ReactVirtualized__Table__headerRow { + font-weight: 700; + display: flex; + flex-direction: row; + align-items: left; + + background: $list-item-bg; + border-top: 2px solid $body-bg; + border-bottom: 2px solid $body-bg; + + color: $blue; +} +.ReactVirtualized__Table__row { + display: flex; + flex-direction: row; + align-items: center; + + border-bottom: 2px solid $body-bg; +} + +.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; + + border-right: 2px solid $body-bg; +} + +.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; +} From f7c1842a2b0f8656669a03237bc847d1f3463a46 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 6 Mar 2019 10:54:47 -0800 Subject: [PATCH 09/31] move rendering to its own file --- .../app/plugins/panel/table2/TablePanel.tsx | 332 +---------------- public/app/plugins/panel/table2/renderer.tsx | 333 ++++++++++++++++++ .../panel/table2/specs/renderer.test.ts | 8 +- 3 files changed, 354 insertions(+), 319 deletions(-) create mode 100644 public/app/plugins/panel/table2/renderer.tsx diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index 1cd00ec6b74..21af18024ba 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -1,16 +1,14 @@ // Libraries import _ from 'lodash'; -import moment from 'moment'; -import React, { Component, CSSProperties, ReactNode } from 'react'; - -import { sanitize } from 'app/core/utils/text'; +import React, { Component, ReactNode } from 'react'; // Types import { PanelProps } from '@grafana/ui/src/types'; -import { Options, Style, CellFormatter, ColumnInfo } from './types'; -import kbn from 'app/core/utils/kbn'; +import { Options } from './types'; import { Table, SortDirectionType, SortIndicator, Column, TableHeaderProps, TableCellProps } from 'react-virtualized'; +import { TableRenderer } from './renderer'; + interface Props extends PanelProps {} interface State { @@ -20,316 +18,20 @@ interface State { } export class TablePanel extends Component { - isUTC: false; // TODO? get UTC from props? - - columns: ColumnInfo[]; - colorState: any; + renderer: TableRenderer; constructor(props: Props) { super(props); this.state = { - sortBy: 'index', + sortBy: 'XX', sortDirection: 'ASC', sortedList: [], }; - } - initColumns() { - this.colorState = {}; + const { panelData, options, replaceVariables } = this.props; + const theme = null; // TODO? - 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); - } - - renderRowVariables(rowIndex) { - const { panelData } = this.props; - - const scopedVars = {}; - const row = panelData.tableData.rows[rowIndex]; - for (let i = 0; i < row.length; i++) { - scopedVars[`__cell_${i}`] = { value: row[i] }; - } - return scopedVars; - } - - renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) { - 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 scopedVars = this.renderRowVariables(rowIndex); - scopedVars['__cell'] = { value: value }; - - const { replaceVariables } = this.props; - - 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 = ( - - {value} - - ); - } else { - columnHtml = {value}; - } - - let filterLink: JSX.Element; - if (column.filterable) { - cellClasses.push('table-panel-cell-filterable'); - filterLink = ( - - - - - - - - - ); - } - - if (cellClasses.length) { - cellClass = cellClasses.join(' '); - } - - style.width = '100%'; - style.height = '100%'; - columnHtml = ( -
- {columnHtml} - {filterLink} -
- ); - return columnHtml; + this.renderer = new TableRenderer(panelData.tableData, options, replaceVariables, theme); } _rowGetter = ({ index }) => { @@ -344,23 +46,25 @@ export class TablePanel extends Component { }; _headerRenderer = (header: TableHeaderProps): ReactNode => { + const { sortBy, dataKey, sortDirection } = header; const tableData = this.props.panelData.tableData!; - const col = tableData.columns[header.dataKey]; + const col = tableData.columns[dataKey]; if (!col) { - return
??{header.dataKey}
; + return
??{dataKey}??
; } return (
- {col.text} {header.sortBy === header.dataKey && } + {col.text} {sortBy === dataKey && }
); }; _cellRenderer = (cell: TableCellProps) => { + const { columnIndex, rowIndex } = cell; const tableData = this.props.panelData.tableData!; - const val = tableData.rows[cell.rowIndex][cell.dataKey]; - return
{val}
; + const val = tableData.rows[rowIndex][columnIndex]; + return this.renderer.renderCell(columnIndex, rowIndex, val); }; render() { @@ -369,18 +73,16 @@ export class TablePanel extends Component { const { sortBy, sortDirection } = this.state; const { tableData } = panelData; - if (!tableData) { + if (!tableData || tableData.rows.length < 1) { return
No Table Data...
; } return ( string; + +interface ColumnInfo { + header: string; + accessor: string; // the field name + style?: Style; + hidden?: boolean; + formatter: CellFormatter; + filterable?: boolean; +} + +export class TableRenderer { + isUTC: false; // TODO? get UTC from props? + + columns: ColumnInfo[]; + colorState: any; + + constructor( + private data: TableData, + options: Options, + private replaceVariables: InterpolateFunction, + private theme?: GrafanaThemeType + ) { + this.colorState = {}; + + if (!data) { + this.columns = []; + return; + } + const { styles } = options; + + this.columns = data.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, style: Style) { + if (!style.thresholds) { + return null; + } + for (let i = style.thresholds.length; i > 0; i--) { + if (value >= style.thresholds[i - 1]) { + return getColorFromHexRgbOrName(style.colors[i], this.theme); + } + } + return getColorFromHexRgbOrName(_.first(style.colors), this.theme); + } + + 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 = getValueFormat(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); + } + + renderRowVariables(rowIndex: number) { + const scopedVars = {}; + const row = this.data.rows[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 scopedVars = this.renderRowVariables(rowIndex); + scopedVars['__cell'] = { value: value }; + + const cellLink = this.replaceVariables(column.style.linkUrl, scopedVars, encodeURIComponent); + const cellLinkTooltip = this.replaceVariables(column.style.linkTooltip, scopedVars); + const cellTarget = column.style.linkTargetBlank ? '_blank' : ''; + + cellClasses.push('table-panel-cell-link'); + columnHtml = ( + + {value} + + ); + } else { + columnHtml = {value}; + } + + let filterLink: JSX.Element; + if (column.filterable) { + cellClasses.push('table-panel-cell-filterable'); + filterLink = ( + + + + + + + + + ); + } + + if (cellClasses.length) { + cellClass = cellClasses.join(' '); + } + + style.width = '100%'; + style.height = '100%'; + columnHtml = ( +
+ {columnHtml} + {filterLink} +
+ ); + return columnHtml; + } +} diff --git a/public/app/plugins/panel/table2/specs/renderer.test.ts b/public/app/plugins/panel/table2/specs/renderer.test.ts index c76ccc8f716..038b7a0f744 100644 --- a/public/app/plugins/panel/table2/specs/renderer.test.ts +++ b/public/app/plugins/panel/table2/specs/renderer.test.ts @@ -1,10 +1,11 @@ import _ from 'lodash'; import TableModel from 'app/core/table_model'; -import { TablePanel } from '../TablePanel'; + import { getColorDefinitionByName } from '@grafana/ui'; import { Options } from '../types'; import { PanelProps, LoadingState } from '@grafana/ui/src/types'; import moment from 'moment'; +import { TableRenderer } from '../renderer'; // TODO: this is commented out with *x* describe! // Essentially all the elements need to replace the
`; - expect(normalize(html)).toBe(normalize(expectedHtml)); + expect(normalize(html + '')).toBe(normalize(expectedHtml)); }); it('Array column should not use number as formatter', () => { @@ -404,6 +387,6 @@ xdescribe('when rendering table', () => { }); }); -function normalize(str) { +function normalize(str: string) { return str.replace(/\s+/gm, ' ').trim(); } diff --git a/public/app/plugins/panel/table2/renderer.tsx b/packages/grafana-ui/src/components/DataTable/renderer.tsx similarity index 83% rename from public/app/plugins/panel/table2/renderer.tsx rename to packages/grafana-ui/src/components/DataTable/renderer.tsx index f05ada55645..fb6263fd4d7 100644 --- a/public/app/plugins/panel/table2/renderer.tsx +++ b/packages/grafana-ui/src/components/DataTable/renderer.tsx @@ -8,33 +8,35 @@ import { sanitize } from 'app/core/utils/text'; // Types import kbn from 'app/core/utils/kbn'; import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType, InterpolateFunction, Column } from '@grafana/ui'; -import { Style } from './types'; import { Index } from 'react-virtualized'; +import { ColumnStyle } from './DataTable'; -type CellFormatter = (v: any, style: Style) => string; +type CellFormatter = (v: any, style?: ColumnStyle) => string | undefined; interface ColumnInfo { header: string; accessor: string; // the field name - style?: Style; + style?: ColumnStyle; hidden?: boolean; formatter: CellFormatter; filterable?: boolean; } -export class TableRenderer { - isUTC: false; // TODO? get UTC from props? +interface RendererOptions { + styles: ColumnStyle[]; + schema: Column[]; + rowGetter: (info: Index) => any[]; // matches the table rowGetter + replaceVariables: InterpolateFunction; + isUTC?: boolean; // TODO? get UTC from props? + theme?: GrafanaThemeType | undefined; +} +export class TableRenderer { columns: ColumnInfo[]; colorState: any; - theme?: GrafanaThemeType; - constructor( - styles: Style[], - schema: Column[], - private rowGetter: (info: Index) => any[], // matches the table rowGetter - private replaceVariables: InterpolateFunction - ) { + constructor(private options: RendererOptions) { + const { schema, styles } = options; this.colorState = {}; if (!schema) { @@ -42,10 +44,11 @@ export class TableRenderer { return; } - this.columns = schema.map((col, index) => { + this.columns = options.schema.map((col, index) => { let title = col.text; - let style: Style = null; + let style; // ColumnStyle + // Find the style based on the text for (let i = 0; i < styles.length; i++) { const s = styles[i]; const regex = kbn.stringToJsRegex(s.pattern); @@ -62,28 +65,24 @@ export class TableRenderer { header: title, accessor: col.text, // unique? style: style, - formatter: this.createColumnFormatter(style, col), + formatter: this.createColumnFormatter(col, style), }; }); } - setTheme(theme: GrafanaThemeType) { - this.theme = theme; - } - - getColorForValue(value, style: Style) { + getColorForValue(value: any, style: ColumnStyle) { if (!style.thresholds) { return null; } for (let i = style.thresholds.length; i > 0; i--) { if (value >= style.thresholds[i - 1]) { - return getColorFromHexRgbOrName(style.colors[i], this.theme); + return getColorFromHexRgbOrName(style.colors![i], this.options.theme); } } - return getColorFromHexRgbOrName(_.first(style.colors), this.theme); + return getColorFromHexRgbOrName(_.first(style.colors), this.options.theme); } - defaultCellFormatter(v: any, style: Style): string { + defaultCellFormatter(v: any, style?: ColumnStyle): string { if (v === null || v === void 0 || v === undefined) { return ''; } @@ -99,7 +98,7 @@ export class TableRenderer { } } - createColumnFormatter(style: Style, header: any): CellFormatter { + createColumnFormatter(header: Column, style?: ColumnStyle): CellFormatter { if (!style) { return this.defaultCellFormatter; } @@ -120,7 +119,7 @@ export class TableRenderer { v = v[0]; } let date = moment(v); - if (this.isUTC) { + if (this.options.isUTC) { date = date.utc(); } return date.format(style.dateFormat); @@ -203,7 +202,7 @@ export class TableRenderer { }; } - setColorState(value: any, style: Style) { + setColorState(value: any, style: ColumnStyle) { if (!style.colorMode) { return; } @@ -220,8 +219,8 @@ export class TableRenderer { } renderRowVariables(rowIndex: number) { - const scopedVars = {}; - const row = this.rowGetter({ index: rowIndex }); + const scopedVars: any = {}; + const row = this.options.rowGetter({ index: rowIndex }); for (let i = 0; i < row.length; i++) { scopedVars[`__cell_${i}`] = { value: row[i] }; } @@ -261,11 +260,12 @@ export class TableRenderer { let columnHtml: JSX.Element; if (column.style && column.style.link) { // Render cell as link + const { replaceVariables } = this.options; const scopedVars = this.renderRowVariables(rowIndex); scopedVars['__cell'] = { value: value }; - const cellLink = this.replaceVariables(column.style.linkUrl, scopedVars, encodeURIComponent); - const cellLinkTooltip = this.replaceVariables(column.style.linkTooltip, scopedVars); + 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'); @@ -284,7 +284,7 @@ export class TableRenderer { columnHtml = {value}; } - let filterLink: JSX.Element; + let filterLink: JSX.Element | null = null; if (column.filterable) { cellClasses.push('table-panel-cell-filterable'); filterLink = ( diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index f5e9f96efba..08872648f44 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -4,7 +4,7 @@ import isNumber from 'lodash/isNumber'; import { colors } from './colors'; // Types -import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; +import { TimeSeries, TableData, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; interface Options { timeSeries: TimeSeries[]; @@ -173,3 +173,24 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS return vmSeries; } + +export function sortTableData(data?: TableData, sortIndex?: number, reverse = false): TableData { + if (data && isNumber(sortIndex)) { + const copy = { + ...data, + rows: [...data.rows].sort((a, b) => { + a = a[sortIndex]; + b = b[sortIndex]; + // Sort null or undefined separately from comparable values + return +(a == null) - +(b == null) || +(a > b) || -(a < b); + }), + }; + + if (reverse) { + copy.rows.reverse(); + } + + return copy; + } + return data; +} diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index b77bea5bd46..ffb8f89b972 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/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 { Style } from '../table2/types'; +import { ColumnStyle } from '@grafana/ui/src/components/DataTable/DataTable'; export class TableRenderer { formatters: any[]; @@ -52,7 +52,7 @@ export class TableRenderer { } } - getColorForValue(value, style: Style) { + getColorForValue(value, style: ColumnStyle) { if (!style.thresholds) { return null; } @@ -64,7 +64,7 @@ export class TableRenderer { return getColorFromHexRgbOrName(_.first(style.colors), this.theme); } - defaultCellFormatter(v, style: Style) { + defaultCellFormatter(v, style: ColumnStyle) { if (v === null || v === void 0 || v === undefined) { return ''; } @@ -191,7 +191,7 @@ export class TableRenderer { }; } - setColorState(value, style: Style) { + setColorState(value, style: ColumnStyle) { if (!style.colorMode) { return; } diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index bca3c350bbb..de4648f2c8b 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -1,136 +1,29 @@ // Libraries import _ from 'lodash'; -import React, { Component, ReactNode } from 'react'; +import React, { Component } from 'react'; // Types -import { PanelProps, ThemeContext, TableData } from '@grafana/ui'; +import { PanelProps, ThemeContext } from '@grafana/ui'; import { Options } from './types'; -import { Table, SortDirectionType, SortIndicator, Column, TableHeaderProps, TableCellProps } from 'react-virtualized'; - -import { TableRenderer } from './renderer'; -import { sortTableData } from './sortable'; +import DataTable from '@grafana/ui/src/components/DataTable/DataTable'; interface Props extends PanelProps {} -interface State { - sortBy?: number; - sortDirection?: SortDirectionType; - data: TableData; -} - -export class TablePanel extends Component { - renderer: TableRenderer; - +export class TablePanel extends Component { constructor(props: Props) { super(props); - - const { panelData, options, replaceVariables } = this.props; - - this.state = { - data: panelData.tableData, - }; - - this.renderer = new TableRenderer(options.styles, this.state.data.columns, this.rowGetter, replaceVariables); } - componentDidUpdate(prevProps: Props, prevState: State) { - const { panelData, options } = this.props; - const { sortBy, sortDirection } = this.state; - - // Update the renderer if options change - if (options !== prevProps.options) { - this.renderer = new TableRenderer( - options.styles, - this.state.data.columns, - this.rowGetter, - this.props.replaceVariables - ); - } - - // Update the data when data or sort changes - if (panelData !== prevProps.panelData || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { - const data = sortTableData(panelData.tableData, sortBy, sortDirection === 'DESC'); - this.setState({ data }); - } - } - - rowGetter = ({ index }) => { - return this.state.data.rows[index]; - }; - - doSort = ({ sortBy }) => { - let sortDirection = this.state.sortDirection; - if (sortBy !== this.state.sortBy) { - sortDirection = 'DESC'; - } else if (sortDirection === 'DESC') { - sortDirection = 'ASC'; - } else { - sortBy = null; - } - - this.setState({ sortBy, sortDirection }); - }; - - 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 ( -
- {col.text} {sortBy === dataKey && } -
- ); - }; - - cellRenderer = (cell: TableCellProps) => { - const { columnIndex, rowIndex } = cell; - const row = this.state.data.rows[rowIndex]; - const val = row[columnIndex]; - return this.renderer.renderCell(columnIndex, rowIndex, val); - }; - render() { - const { width, height, options } = this.props; - const { showHeader } = options; - // const { sortBy, sortDirection } = this.state; - const { data } = this.state; + const { panelData, options } = this.props; - if (!data) { + if (!panelData || !panelData.tableData) { return
No Table Data...
; } return ( - {( - theme // ??? { this.renderer.setTheme(theme) } - ) => ( -
with
@@ -202,9 +203,8 @@ xdescribe('when rendering table', () => { renderCounter: 1, options: panel, }; - - const renderer = new TablePanel(props); //panel, table, 'utc', sanitize, templateSrv); - renderer.render(); // This will initalize + const theme = null; + const renderer = new TableRenderer(table, panel, props.replaceVariables, theme); //panel, table, 'utc', sanitize, templateSrv); it('time column should be formated', () => { const html = renderer.renderCell(0, 0, 1388556366666); From 0d8384e05d09a08eb077bca2e577a44cfec1f5e0 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 6 Mar 2019 14:39:04 -0800 Subject: [PATCH 10/31] sortable class --- .../app/plugins/panel/table2/TablePanel.tsx | 134 +++++++++++------- public/app/plugins/panel/table2/renderer.tsx | 30 ++-- public/app/plugins/panel/table2/sortable.tsx | 41 ++++++ .../panel/table2/specs/renderer.test.ts | 7 +- public/app/plugins/panel/table2/types.ts | 11 -- public/sass/components/_panel_table2.scss | 8 +- 6 files changed, 150 insertions(+), 81 deletions(-) create mode 100644 public/app/plugins/panel/table2/sortable.tsx diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index 21af18024ba..cf9ddd83094 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -3,18 +3,19 @@ import _ from 'lodash'; import React, { Component, ReactNode } from 'react'; // Types -import { PanelProps } from '@grafana/ui/src/types'; +import { PanelProps, ThemeContext } from '@grafana/ui'; import { Options } from './types'; import { Table, SortDirectionType, SortIndicator, Column, TableHeaderProps, TableCellProps } from 'react-virtualized'; import { TableRenderer } from './renderer'; +import { SortedTableData } from './sortable'; interface Props extends PanelProps {} interface State { - sortBy: string; - sortDirection: SortDirectionType; - sortedList: any[]; + sortBy?: number; // but really is a number! + sortDirection?: SortDirectionType; + data: SortedTableData; } export class TablePanel extends Component { @@ -22,55 +23,87 @@ export class TablePanel extends Component { constructor(props: Props) { super(props); - this.state = { - sortBy: 'XX', - sortDirection: 'ASC', - sortedList: [], - }; const { panelData, options, replaceVariables } = this.props; - const theme = null; // TODO? - this.renderer = new TableRenderer(panelData.tableData, options, replaceVariables, theme); + this.state = { + data: new SortedTableData(panelData.tableData), + }; + + this.renderer = new TableRenderer(options.styles, this.state.data, this._rowGetter, replaceVariables); + } + + componentDidUpdate(prevProps: Props, prevState: State) { + const { panelData, options } = this.props; + const { sortBy, sortDirection } = this.state; + + console.log('componentDidUpdate', this.props); + + // Update the renderer if options change + if (options !== prevProps.options) { + console.log('Options Changed, update renderer', options); + this.renderer = new TableRenderer(options.styles, this.state.data, this._rowGetter, this.props.replaceVariables); + } + + // Update the data when data or sort changes + if (panelData !== prevProps.panelData || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { + const data = new SortedTableData(panelData.tableData, sortBy, sortDirection === 'DESC'); + this.setState({ data }); + console.log('Data Changed, update', data); + } } _rowGetter = ({ index }) => { - return this.props.panelData.tableData.rows[index]; + return this.state.data.getRow(index); }; - _sort = ({ sortBy, sortDirection }) => { - // const sortedList = this._sortList({sortBy, sortDirection}); + _sort = ({ sortBy }) => { + let sortDirection = this.state.sortDirection; + if (sortBy !== this.state.sortBy) { + sortDirection = 'DESC'; + } else if (sortDirection === 'DESC') { + sortDirection = 'ASC'; + } else { + sortBy = null; + } - // this.setState({sortBy, sortDirection, sortedList}); - console.log('TODO, sort!', sortBy, sortDirection); + // This will trigger sort via properties + console.log('SORT', sortBy, typeof sortBy, sortDirection); + + this.setState({ sortBy, sortDirection }); }; _headerRenderer = (header: TableHeaderProps): ReactNode => { - const { sortBy, dataKey, sortDirection } = header; - const tableData = this.props.panelData.tableData!; - const col = tableData.columns[dataKey]; + const dataKey = header.dataKey as any; // types say string, but it is number? + const { data, sortBy, sortDirection } = this.state; + + const col = data.getInfo()[dataKey]; if (!col) { return
??{dataKey}??
; } + const isSorted = sortBy === dataKey; + + console.log('header SORT', sortBy, isSorted); + return (
- {col.text} {sortBy === dataKey && } + {col.text} {isSorted && }
); }; _cellRenderer = (cell: TableCellProps) => { const { columnIndex, rowIndex } = cell; - const tableData = this.props.panelData.tableData!; - const val = tableData.rows[rowIndex][columnIndex]; + const row = this.state.data.getRow(rowIndex); + const val = row[columnIndex]; return this.renderer.renderCell(columnIndex, rowIndex, val); }; render() { const { panelData, width, height, options } = this.props; const { showHeader } = options; - const { sortBy, sortDirection } = this.state; + // const { sortBy, sortDirection } = this.state; const { tableData } = panelData; if (!tableData || tableData.rows.length < 1) { @@ -78,32 +111,35 @@ export class TablePanel extends Component { } return ( - - {tableData.columns.map((col, index) => { - return ( - - ); - })} -
+ + {( + theme // ??? { this.renderer.setTheme(theme) } + ) => ( + + {tableData.columns.map((col, index) => { + return ( + + ); + })} +
+ )} +
); } } diff --git a/public/app/plugins/panel/table2/renderer.tsx b/public/app/plugins/panel/table2/renderer.tsx index f5779b13f0d..a663a5b1a05 100644 --- a/public/app/plugins/panel/table2/renderer.tsx +++ b/public/app/plugins/panel/table2/renderer.tsx @@ -7,14 +7,10 @@ import { sanitize } from 'app/core/utils/text'; // Types import kbn from 'app/core/utils/kbn'; -import { - getValueFormat, - getColorFromHexRgbOrName, - GrafanaThemeType, - InterpolateFunction, - TableData, -} from '@grafana/ui'; -import { Options, Style } from './types'; +import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType, InterpolateFunction } from '@grafana/ui'; +import { Style } from './types'; +import { SortedTableData } from './sortable'; +import { Index } from 'react-virtualized'; type CellFormatter = (v: any, style: Style) => string; @@ -32,12 +28,13 @@ export class TableRenderer { columns: ColumnInfo[]; colorState: any; + theme?: GrafanaThemeType; constructor( - private data: TableData, - options: Options, - private replaceVariables: InterpolateFunction, - private theme?: GrafanaThemeType + styles: Style[], + data: SortedTableData, + private rowGetter: (info: Index) => any[], // matches the table rowGetter + private replaceVariables: InterpolateFunction ) { this.colorState = {}; @@ -45,9 +42,8 @@ export class TableRenderer { this.columns = []; return; } - const { styles } = options; - this.columns = data.columns.map((col, index) => { + this.columns = data.getInfo().map((col, index) => { let title = col.text; let style: Style = null; @@ -72,6 +68,10 @@ export class TableRenderer { }); } + setTheme(theme: GrafanaThemeType) { + this.theme = theme; + } + getColorForValue(value, style: Style) { if (!style.thresholds) { return null; @@ -222,7 +222,7 @@ export class TableRenderer { renderRowVariables(rowIndex: number) { const scopedVars = {}; - const row = this.data.rows[rowIndex]; + const row = this.rowGetter({ index: rowIndex }); for (let i = 0; i < row.length; i++) { scopedVars[`__cell_${i}`] = { value: row[i] }; } diff --git a/public/app/plugins/panel/table2/sortable.tsx b/public/app/plugins/panel/table2/sortable.tsx new file mode 100644 index 00000000000..90e4fd961cb --- /dev/null +++ b/public/app/plugins/panel/table2/sortable.tsx @@ -0,0 +1,41 @@ +// Libraries +import _ from 'lodash'; + +import { TableData } from '@grafana/ui'; + +export class SortedTableData { + rows: any[]; + + constructor(private data: TableData, sortIndex?: number, reverse?: boolean) { + if (_.isNumber(sortIndex)) { + // Make a copy of all the rows + this.rows = this.data.rows.map((row, index) => { + return row; + }); + this.rows.sort((a, b) => { + a = a[sortIndex]; + b = b[sortIndex]; + // Sort null or undefined separately from comparable values + return +(a == null) - +(b == null) || +(a > b) || -(a < b); + }); + + if (reverse) { + this.rows.reverse(); + } + } else { + this.rows = data.rows; + } + } + + getInfo(): any[] { + return this.data.columns; + } + + getRow(index: number): any[] { + return this.rows[index]; + } + + getCount(): number { + return this.rows.length; + } +} diff --git a/public/app/plugins/panel/table2/specs/renderer.test.ts b/public/app/plugins/panel/table2/specs/renderer.test.ts index 038b7a0f744..f826ec20649 100644 --- a/public/app/plugins/panel/table2/specs/renderer.test.ts +++ b/public/app/plugins/panel/table2/specs/renderer.test.ts @@ -6,6 +6,7 @@ import { Options } from '../types'; import { PanelProps, LoadingState } from '@grafana/ui/src/types'; import moment from 'moment'; import { TableRenderer } from '../renderer'; +import { SortedTableData } from '../sortable'; // TODO: this is commented out with *x* describe! // Essentially all the elements need to replace the
with
@@ -203,8 +204,10 @@ xdescribe('when rendering table', () => { renderCounter: 1, options: panel, }; - const theme = null; - const renderer = new TableRenderer(table, panel, props.replaceVariables, theme); //panel, table, 'utc', sanitize, templateSrv); + const data = new SortedTableData(table); + const rowGetter = ({ index }) => data.getRow(index); + const renderer = new TableRenderer(panel.styles, data, rowGetter, props.replaceVariables); + renderer.setTheme(null); it('time column should be formated', () => { const html = renderer.renderCell(0, 0, 1388556366666); diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index 6ff384b06e0..c0a3b2c8561 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/public/app/plugins/panel/table2/types.ts @@ -22,17 +22,6 @@ export interface Style { preserveFormat?: boolean; } -export type CellFormatter = (v: any, style: Style) => string; - -export interface ColumnInfo { - header: string; - accessor: string; // the field name - style?: Style; - hidden?: boolean; - formatter: CellFormatter; - filterable?: boolean; -} - export interface Options { showHeader: boolean; styles: Style[]; diff --git a/public/sass/components/_panel_table2.scss b/public/sass/components/_panel_table2.scss index 891588a8687..b0aa2d6b742 100644 --- a/public/sass/components/_panel_table2.scss +++ b/public/sass/components/_panel_table2.scss @@ -1,8 +1,8 @@ -.ReactVirtualized__Table { -} +// .ReactVirtualized__Table { +// } -.ReactVirtualized__Table__Grid { -} +// .ReactVirtualized__Table__Grid { +// } .ReactVirtualized__Table__headerRow { font-weight: 700; From 23c3e9d80a468b5bdd7e3500ae495da680aa3966 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 6 Mar 2019 16:25:28 -0800 Subject: [PATCH 11/31] cleanup --- .../app/plugins/panel/table2/TablePanel.tsx | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index cf9ddd83094..098a4bd72a1 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -13,7 +13,7 @@ import { SortedTableData } from './sortable'; interface Props extends PanelProps {} interface State { - sortBy?: number; // but really is a number! + sortBy?: number; sortDirection?: SortDirectionType; data: SortedTableData; } @@ -37,11 +37,8 @@ export class TablePanel extends Component { const { panelData, options } = this.props; const { sortBy, sortDirection } = this.state; - console.log('componentDidUpdate', this.props); - // Update the renderer if options change if (options !== prevProps.options) { - console.log('Options Changed, update renderer', options); this.renderer = new TableRenderer(options.styles, this.state.data, this._rowGetter, this.props.replaceVariables); } @@ -49,7 +46,6 @@ export class TablePanel extends Component { if (panelData !== prevProps.panelData || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { const data = new SortedTableData(panelData.tableData, sortBy, sortDirection === 'DESC'); this.setState({ data }); - console.log('Data Changed, update', data); } } @@ -74,21 +70,13 @@ export class TablePanel extends Component { }; _headerRenderer = (header: TableHeaderProps): ReactNode => { - const dataKey = header.dataKey as any; // types say string, but it is number? + const dataKey = header.dataKey as any; // types say string, but it is number! const { data, sortBy, sortDirection } = this.state; - const col = data.getInfo()[dataKey]; - if (!col) { - return
??{dataKey}??
; - } - - const isSorted = sortBy === dataKey; - - console.log('header SORT', sortBy, isSorted); return (
- {col.text} {isSorted && } + {col.text} {sortBy === dataKey && }
); }; @@ -101,12 +89,12 @@ export class TablePanel extends Component { }; render() { - const { panelData, width, height, options } = this.props; + const { width, height, options } = this.props; const { showHeader } = options; // const { sortBy, sortDirection } = this.state; - const { tableData } = panelData; + const { data } = this.state; - if (!tableData || tableData.rows.length < 1) { + if (!data) { return
No Table Data...
; } @@ -122,18 +110,21 @@ export class TablePanel extends Component { overscanRowCount={10} rowHeight={30} rowGetter={this._rowGetter} - rowCount={tableData.rows.length} + rowCount={data.getCount()} sort={this._sort} width={width} > - {tableData.columns.map((col, index) => { + {data.getInfo().map((col, index) => { return ( ); })} From 3b31341ad923ea6a76d995ae0415906ff64eb3ac Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 6 Mar 2019 16:34:32 -0800 Subject: [PATCH 12/31] remove _ --- .../app/plugins/panel/table2/TablePanel.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index 098a4bd72a1..0c06d9cf23f 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -30,7 +30,7 @@ export class TablePanel extends Component { data: new SortedTableData(panelData.tableData), }; - this.renderer = new TableRenderer(options.styles, this.state.data, this._rowGetter, replaceVariables); + this.renderer = new TableRenderer(options.styles, this.state.data, this.rowGetter, replaceVariables); } componentDidUpdate(prevProps: Props, prevState: State) { @@ -39,7 +39,7 @@ export class TablePanel extends Component { // Update the renderer if options change if (options !== prevProps.options) { - this.renderer = new TableRenderer(options.styles, this.state.data, this._rowGetter, this.props.replaceVariables); + this.renderer = new TableRenderer(options.styles, this.state.data, this.rowGetter, this.props.replaceVariables); } // Update the data when data or sort changes @@ -49,11 +49,11 @@ export class TablePanel extends Component { } } - _rowGetter = ({ index }) => { + rowGetter = ({ index }) => { return this.state.data.getRow(index); }; - _sort = ({ sortBy }) => { + doSort = ({ sortBy }) => { let sortDirection = this.state.sortDirection; if (sortBy !== this.state.sortBy) { sortDirection = 'DESC'; @@ -69,7 +69,7 @@ export class TablePanel extends Component { this.setState({ sortBy, sortDirection }); }; - _headerRenderer = (header: TableHeaderProps): ReactNode => { + 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.getInfo()[dataKey]; @@ -81,7 +81,7 @@ export class TablePanel extends Component { ); }; - _cellRenderer = (cell: TableCellProps) => { + cellRenderer = (cell: TableCellProps) => { const { columnIndex, rowIndex } = cell; const row = this.state.data.getRow(rowIndex); const val = row[columnIndex]; @@ -109,9 +109,9 @@ export class TablePanel extends Component { height={height} overscanRowCount={10} rowHeight={30} - rowGetter={this._rowGetter} + rowGetter={this.rowGetter} rowCount={data.getCount()} - sort={this._sort} + sort={this.doSort} width={width} > {data.getInfo().map((col, index) => { @@ -119,11 +119,10 @@ export class TablePanel extends Component { ); From 1609c07fb2ebadf1e2a10571a7a0f3474d11726d Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 14:55:30 -0800 Subject: [PATCH 13/31] use TableData, not interface --- packages/grafana-ui/src/types/data.ts | 2 +- .../app/plugins/panel/table2/TablePanel.tsx | 32 ++++++------ public/app/plugins/panel/table2/renderer.tsx | 9 ++-- public/app/plugins/panel/table2/sortable.tsx | 50 +++++++------------ .../panel/table2/specs/renderer.test.ts | 6 +-- 5 files changed, 43 insertions(+), 56 deletions(-) diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index 1e4ccba3948..e7e0bdc2b4e 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -53,7 +53,7 @@ export interface TimeSeriesVMs { length: number; } -interface Column { +export interface Column { text: string; title?: string; type?: string; diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index 0c06d9cf23f..bca3c350bbb 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -3,19 +3,19 @@ import _ from 'lodash'; import React, { Component, ReactNode } from 'react'; // Types -import { PanelProps, ThemeContext } from '@grafana/ui'; +import { PanelProps, ThemeContext, TableData } from '@grafana/ui'; import { Options } from './types'; import { Table, SortDirectionType, SortIndicator, Column, TableHeaderProps, TableCellProps } from 'react-virtualized'; import { TableRenderer } from './renderer'; -import { SortedTableData } from './sortable'; +import { sortTableData } from './sortable'; interface Props extends PanelProps {} interface State { sortBy?: number; sortDirection?: SortDirectionType; - data: SortedTableData; + data: TableData; } export class TablePanel extends Component { @@ -27,10 +27,10 @@ export class TablePanel extends Component { const { panelData, options, replaceVariables } = this.props; this.state = { - data: new SortedTableData(panelData.tableData), + data: panelData.tableData, }; - this.renderer = new TableRenderer(options.styles, this.state.data, this.rowGetter, replaceVariables); + this.renderer = new TableRenderer(options.styles, this.state.data.columns, this.rowGetter, replaceVariables); } componentDidUpdate(prevProps: Props, prevState: State) { @@ -39,18 +39,23 @@ export class TablePanel extends Component { // Update the renderer if options change if (options !== prevProps.options) { - this.renderer = new TableRenderer(options.styles, this.state.data, this.rowGetter, this.props.replaceVariables); + this.renderer = new TableRenderer( + options.styles, + this.state.data.columns, + this.rowGetter, + this.props.replaceVariables + ); } // Update the data when data or sort changes if (panelData !== prevProps.panelData || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { - const data = new SortedTableData(panelData.tableData, sortBy, sortDirection === 'DESC'); + const data = sortTableData(panelData.tableData, sortBy, sortDirection === 'DESC'); this.setState({ data }); } } rowGetter = ({ index }) => { - return this.state.data.getRow(index); + return this.state.data.rows[index]; }; doSort = ({ sortBy }) => { @@ -63,16 +68,13 @@ export class TablePanel extends Component { sortBy = null; } - // This will trigger sort via properties - console.log('SORT', sortBy, typeof sortBy, sortDirection); - this.setState({ sortBy, sortDirection }); }; 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.getInfo()[dataKey]; + const col = data.columns[dataKey]; return (
@@ -83,7 +85,7 @@ export class TablePanel extends Component { cellRenderer = (cell: TableCellProps) => { const { columnIndex, rowIndex } = cell; - const row = this.state.data.getRow(rowIndex); + const row = this.state.data.rows[rowIndex]; const val = row[columnIndex]; return this.renderer.renderCell(columnIndex, rowIndex, val); }; @@ -110,11 +112,11 @@ export class TablePanel extends Component { overscanRowCount={10} rowHeight={30} rowGetter={this.rowGetter} - rowCount={data.getCount()} + rowCount={data.rows.length} sort={this.doSort} width={width} > - {data.getInfo().map((col, index) => { + {data.columns.map((col, index) => { return ( string; @@ -32,18 +31,18 @@ export class TableRenderer { constructor( styles: Style[], - data: SortedTableData, + schema: Column[], private rowGetter: (info: Index) => any[], // matches the table rowGetter private replaceVariables: InterpolateFunction ) { this.colorState = {}; - if (!data) { + if (!schema) { this.columns = []; return; } - this.columns = data.getInfo().map((col, index) => { + this.columns = schema.map((col, index) => { let title = col.text; let style: Style = null; diff --git a/public/app/plugins/panel/table2/sortable.tsx b/public/app/plugins/panel/table2/sortable.tsx index 90e4fd961cb..f83df1ce830 100644 --- a/public/app/plugins/panel/table2/sortable.tsx +++ b/public/app/plugins/panel/table2/sortable.tsx @@ -1,41 +1,29 @@ // Libraries -import _ from 'lodash'; +import isNumber from 'lodash/isNumber'; import { TableData } from '@grafana/ui'; -export class SortedTableData { - rows: any[]; - - constructor(private data: TableData, sortIndex?: number, reverse?: boolean) { - if (_.isNumber(sortIndex)) { - // Make a copy of all the rows - this.rows = this.data.rows.map((row, index) => { +export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData { + if (isNumber(sortIndex)) { + const copy = { + ...data, + rows: data.rows.map((row, index) => { return row; - }); - this.rows.sort((a, b) => { - a = a[sortIndex]; - b = b[sortIndex]; - // Sort null or undefined separately from comparable values - return +(a == null) - +(b == null) || +(a > b) || -(a < b); - }); + }), + }; - if (reverse) { - this.rows.reverse(); - } - } else { - this.rows = data.rows; + copy.rows.sort((a, b) => { + a = a[sortIndex]; + b = b[sortIndex]; + // Sort null or undefined separately from comparable values + return +(a == null) - +(b == null) || +(a > b) || -(a < b); + }); + + if (reverse) { + copy.rows.reverse(); } - } - getInfo(): any[] { - return this.data.columns; - } - - getRow(index: number): any[] { - return this.rows[index]; - } - - getCount(): number { - return this.rows.length; + return copy; } + return data; } diff --git a/public/app/plugins/panel/table2/specs/renderer.test.ts b/public/app/plugins/panel/table2/specs/renderer.test.ts index f826ec20649..bbc57d99f2f 100644 --- a/public/app/plugins/panel/table2/specs/renderer.test.ts +++ b/public/app/plugins/panel/table2/specs/renderer.test.ts @@ -6,7 +6,6 @@ import { Options } from '../types'; import { PanelProps, LoadingState } from '@grafana/ui/src/types'; import moment from 'moment'; import { TableRenderer } from '../renderer'; -import { SortedTableData } from '../sortable'; // TODO: this is commented out with *x* describe! // Essentially all the elements need to replace the
with
@@ -204,9 +203,8 @@ xdescribe('when rendering table', () => { renderCounter: 1, options: panel, }; - const data = new SortedTableData(table); - const rowGetter = ({ index }) => data.getRow(index); - const renderer = new TableRenderer(panel.styles, data, rowGetter, props.replaceVariables); + const rowGetter = ({ index }) => table.rows[index]; + const renderer = new TableRenderer(panel.styles, table.columns, rowGetter, props.replaceVariables); renderer.setTheme(null); it('time column should be formated', () => { From 39be5959b8ca18d19621a264a025ce18e44012bd Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 15:01:36 -0800 Subject: [PATCH 14/31] better sort function --- public/app/plugins/panel/table2/sortable.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/panel/table2/sortable.tsx b/public/app/plugins/panel/table2/sortable.tsx index f83df1ce830..24253b54708 100644 --- a/public/app/plugins/panel/table2/sortable.tsx +++ b/public/app/plugins/panel/table2/sortable.tsx @@ -7,18 +7,14 @@ export function sortTableData(data: TableData, sortIndex?: number, reverse = fal if (isNumber(sortIndex)) { const copy = { ...data, - rows: data.rows.map((row, index) => { - return row; + rows: [...data.rows].sort((a, b) => { + a = a[sortIndex]; + b = b[sortIndex]; + // Sort null or undefined separately from comparable values + return +(a == null) - +(b == null) || +(a > b) || -(a < b); }), }; - copy.rows.sort((a, b) => { - a = a[sortIndex]; - b = b[sortIndex]; - // Sort null or undefined separately from comparable values - return +(a == null) - +(b == null) || +(a > b) || -(a < b); - }); - if (reverse) { copy.rows.reverse(); } From d7b1fd75e367dc775c8de7bf2c423d7028170a72 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 16:52:20 -0800 Subject: [PATCH 15/31] move to grafana/ui --- .../src/components/DataTable/DataTable.tsx | 186 ++++++++++ .../components/DataTable}/renderer.test.ts | 327 +++++++++--------- .../src/components/DataTable}/renderer.tsx | 62 ++-- .../grafana-ui/src/utils/processTimeSeries.ts | 23 +- public/app/plugins/panel/table/renderer.ts | 8 +- .../app/plugins/panel/table2/TablePanel.tsx | 121 +------ .../plugins/panel/table2/TablePanelEditor.tsx | 11 +- public/app/plugins/panel/table2/sortable.tsx | 25 -- public/app/plugins/panel/table2/types.ts | 28 +- 9 files changed, 409 insertions(+), 382 deletions(-) create mode 100644 packages/grafana-ui/src/components/DataTable/DataTable.tsx rename {public/app/plugins/panel/table2/specs => packages/grafana-ui/src/components/DataTable}/renderer.test.ts (67%) rename {public/app/plugins/panel/table2 => packages/grafana-ui/src/components/DataTable}/renderer.tsx (83%) delete mode 100644 public/app/plugins/panel/table2/sortable.tsx diff --git a/packages/grafana-ui/src/components/DataTable/DataTable.tsx b/packages/grafana-ui/src/components/DataTable/DataTable.tsx new file mode 100644 index 00000000000..a14522fd10f --- /dev/null +++ b/packages/grafana-ui/src/components/DataTable/DataTable.tsx @@ -0,0 +1,186 @@ +// Libraries +import React, { Component, ReactNode } from 'react'; +import { + Table, + SortDirectionType, + SortIndicator, + Column, + TableHeaderProps, + TableCellProps, + Index, +} from 'react-virtualized'; +import { Themeable } from '../../types/theme'; + +import { sortTableData } from '../../utils/processTimeSeries'; + +// Types +import { TableData, InterpolateFunction } from '../../types/index'; +import { TableRenderer } from './renderer'; + +// Made to match the existing (untyped) settings in the angular table +export interface ColumnStyle { + pattern?: string; + + alias?: string; + colorMode?: string; + colors?: any[]; + decimals?: number; + 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; +} + +interface Props extends Themeable { + data?: TableData; + showHeader: boolean; + styles: ColumnStyle[]; + replaceVariables: InterpolateFunction; + width: number; + height: number; +} + +interface State { + sortBy?: number; + sortDirection?: SortDirectionType; + data?: TableData; +} + +export class DataTable extends Component { + renderer: TableRenderer; + + static defaultProps = { + showHeader: true, + }; + + constructor(props: Props) { + super(props); + + this.state = { + data: props.data, + }; + + this.renderer = this.createRenderer(); + } + + 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.renderer = this.createRenderer(); + } + + // Update the data when data or sort changes + if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { + this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') }); + } + } + + // styles: ColumnStyle[], + // schema: Column[], + // rowGetter: (info: Index) => any[], // matches the table rowGetter + // replaceVariables: InterpolateFunction, + // isUTC?: boolean, // TODO? get UTC from props? + // theme?: GrafanaThemeType | undefined, + + createRenderer(): TableRenderer { + const { styles, replaceVariables, theme } = this.props; + const { data } = this.state; + + return new TableRenderer({ + styles, + schema: data ? data.columns : [], + rowGetter: this.rowGetter, + replaceVariables, + isUTC: false, + theme: theme.type, + }); + } + + 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 ( +
+ {col.text} {sortBy === dataKey && } +
+ ); + }; + + cellRenderer = (cell: TableCellProps) => { + const { columnIndex, rowIndex } = cell; + const row = this.state.data!.rows[rowIndex]; + const val = row[columnIndex]; + return this.renderer.renderCell(columnIndex, rowIndex, val); + }; + + render() { + const { width, height, showHeader } = this.props; + const { data } = this.props; + if (!data) { + return
NO Data
; + } + return ( + + {data.columns.map((col, index) => { + return ( + + ); + })} +
+ ); + } +} + +export default DataTable; diff --git a/public/app/plugins/panel/table2/specs/renderer.test.ts b/packages/grafana-ui/src/components/DataTable/renderer.test.ts similarity index 67% rename from public/app/plugins/panel/table2/specs/renderer.test.ts rename to packages/grafana-ui/src/components/DataTable/renderer.test.ts index bbc57d99f2f..fdb7dc49e4d 100644 --- a/public/app/plugins/panel/table2/specs/renderer.test.ts +++ b/packages/grafana-ui/src/components/DataTable/renderer.test.ts @@ -2,10 +2,11 @@ import _ from 'lodash'; import TableModel from 'app/core/table_model'; import { getColorDefinitionByName } from '@grafana/ui'; -import { Options } from '../types'; -import { PanelProps, LoadingState } from '@grafana/ui/src/types'; +import { ScopedVars } from '@grafana/ui/src/types'; import moment from 'moment'; -import { TableRenderer } from '../renderer'; +import { TableRenderer } from './renderer'; +import { Index } from 'react-virtualized'; +import { ColumnStyle } from './DataTable'; // TODO: this is commented out with *x* describe! // Essentially all the elements need to replace the
with
@@ -33,179 +34,161 @@ xdescribe('when rendering table', () => { [1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2], ]; - const panel: Options = { - showHeader: true, - pageSize: 10, - styles: [ - { - pattern: 'Time', - type: 'date', - alias: 'Timestamp', - }, - { - pattern: '/(Val)ue/', - type: 'number', - unit: 'ms', - decimals: 3, - alias: '$1', - }, - { - pattern: 'Colored', - type: 'number', - unit: 'none', - decimals: 1, - colorMode: 'value', - thresholds: [50, 80], - colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], - }, - { - pattern: 'String', - type: 'string', - }, - { - pattern: 'String', - type: 'string', - }, - { - pattern: 'United', - type: 'number', - unit: 'ms', - decimals: 2, - }, - { - pattern: 'Sanitized', - type: 'string', - sanitize: true, - }, - { - pattern: 'Link', - type: 'string', - link: true, - linkUrl: '/dashboard?param=$__cell¶m_1=$__cell_1¶m_2=$__cell_2', - linkTooltip: '$__cell $__cell_1 $__cell_6', - linkTargetBlank: true, - }, - { - pattern: 'Array', - type: 'number', - unit: 'ms', - decimals: 3, - }, - { - pattern: 'Mapping', - type: 'string', - mappingType: 1, - valueMaps: [ - { - value: '1', - text: 'on', - }, - { - value: '0', - text: 'off', - }, - { - value: 'HELLO WORLD', - text: 'HELLO GRAFANA', - }, - { - value: 'value1, value2', - text: 'value3, value4', - }, - ], - }, - { - pattern: 'RangeMapping', - type: 'string', - mappingType: 2, - rangeMaps: [ - { - from: '1', - to: '3', - text: 'on', - }, - { - from: '3', - to: '6', - text: 'off', - }, - ], - }, - { - pattern: 'MappingColored', - type: 'string', - mappingType: 1, - valueMaps: [ - { - value: '1', - text: 'on', - }, - { - value: '0', - text: 'off', - }, - ], - colorMode: 'value', - thresholds: [1, 2], - colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], - }, - { - pattern: 'RangeMappingColored', - type: 'string', - mappingType: 2, - rangeMaps: [ - { - from: '1', - to: '3', - text: 'on', - }, - { - from: '3', - to: '6', - text: 'off', - }, - ], - colorMode: 'value', - thresholds: [2, 5], - colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], - }, - ], - }; + const styles: ColumnStyle[] = [ + { + pattern: 'Time', + type: 'date', + alias: 'Timestamp', + }, + { + pattern: '/(Val)ue/', + type: 'number', + unit: 'ms', + decimals: 3, + alias: '$1', + }, + { + pattern: 'Colored', + type: 'number', + unit: 'none', + decimals: 1, + colorMode: 'value', + thresholds: [50, 80], + colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], + }, + { + pattern: 'String', + type: 'string', + }, + { + pattern: 'String', + type: 'string', + }, + { + pattern: 'United', + type: 'number', + unit: 'ms', + decimals: 2, + }, + { + pattern: 'Sanitized', + type: 'string', + sanitize: true, + }, + { + pattern: 'Link', + type: 'string', + link: true, + linkUrl: '/dashboard?param=$__cell¶m_1=$__cell_1¶m_2=$__cell_2', + linkTooltip: '$__cell $__cell_1 $__cell_6', + linkTargetBlank: true, + }, + { + pattern: 'Array', + type: 'number', + unit: 'ms', + decimals: 3, + }, + { + pattern: 'Mapping', + type: 'string', + mappingType: 1, + valueMaps: [ + { + value: '1', + text: 'on', + }, + { + value: '0', + text: 'off', + }, + { + value: 'HELLO WORLD', + text: 'HELLO GRAFANA', + }, + { + value: 'value1, value2', + text: 'value3, value4', + }, + ], + }, + { + pattern: 'RangeMapping', + type: 'string', + mappingType: 2, + rangeMaps: [ + { + from: '1', + to: '3', + text: 'on', + }, + { + from: '3', + to: '6', + text: 'off', + }, + ], + }, + { + pattern: 'MappingColored', + type: 'string', + mappingType: 1, + valueMaps: [ + { + value: '1', + text: 'on', + }, + { + value: '0', + text: 'off', + }, + ], + colorMode: 'value', + thresholds: [1, 2], + colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], + }, + { + pattern: 'RangeMappingColored', + type: 'string', + mappingType: 2, + rangeMaps: [ + { + from: '1', + to: '3', + text: 'on', + }, + { + from: '3', + to: '6', + text: 'off', + }, + ], + colorMode: 'value', + thresholds: [2, 5], + colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], + }, + ]; // const sanitize = value => { // return 'sanitized'; // }; - const props: PanelProps = { - panelData: { - tableData: table, - }, - width: 100, - height: 100, - timeRange: { - from: moment(), - to: moment(), - raw: { - from: moment(), - to: moment(), - }, - }, - loading: LoadingState.Done, - replaceVariables: (value, scopedVars) => { - if (scopedVars) { - // For testing variables replacement in link - _.each(scopedVars, (val, key) => { - value = value.replace('$' + key, val.value); - }); - } - return value; - }, - renderCounter: 1, - options: panel, + 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 rowGetter = ({ index }) => table.rows[index]; - const renderer = new TableRenderer(panel.styles, table.columns, rowGetter, props.replaceVariables); - renderer.setTheme(null); + const rowGetter = ({ index }: Index) => table.rows[index]; + const renderer = new TableRenderer({ + styles, + schema: table.columns, + rowGetter, + replaceVariables, + }); it('time column should be formated', () => { const html = renderer.renderCell(0, 0, 1388556366666); @@ -314,7 +297,7 @@ xdescribe('when rendering table', () => {
- {data.columns.map((col, index) => { - return ( - - ); - })} -
- )} + {theme => } ); } diff --git a/public/app/plugins/panel/table2/TablePanelEditor.tsx b/public/app/plugins/panel/table2/TablePanelEditor.tsx index fc899bd22d2..60d2eff9b85 100644 --- a/public/app/plugins/panel/table2/TablePanelEditor.tsx +++ b/public/app/plugins/panel/table2/TablePanelEditor.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelEditorProps, Switch, FormField } from '@grafana/ui'; +import { PanelEditorProps, Switch } from '@grafana/ui'; import { Options } from './types'; export class TablePanelEditor extends PureComponent> { @@ -11,10 +11,8 @@ export class TablePanelEditor extends PureComponent> { 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, pageSize } = this.props.options; + const { showHeader } = this.props.options; return (
@@ -22,11 +20,6 @@ export class TablePanelEditor extends PureComponent> {
Header
- -
-
Paging
- -
); } diff --git a/public/app/plugins/panel/table2/sortable.tsx b/public/app/plugins/panel/table2/sortable.tsx deleted file mode 100644 index 24253b54708..00000000000 --- a/public/app/plugins/panel/table2/sortable.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Libraries -import isNumber from 'lodash/isNumber'; - -import { TableData } from '@grafana/ui'; - -export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData { - if (isNumber(sortIndex)) { - const copy = { - ...data, - rows: [...data.rows].sort((a, b) => { - a = a[sortIndex]; - b = b[sortIndex]; - // Sort null or undefined separately from comparable values - return +(a == null) - +(b == null) || +(a > b) || -(a < b); - }), - }; - - if (reverse) { - copy.rows.reverse(); - } - - return copy; - } - return data; -} diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index c0a3b2c8561..0935a0878c9 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/public/app/plugins/panel/table2/types.ts @@ -1,31 +1,8 @@ -// 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; -} +import { ColumnStyle } from '@grafana/ui/src/components/DataTable/DataTable'; export interface Options { showHeader: boolean; - styles: Style[]; - pageSize: number; + styles: ColumnStyle[]; } export const defaults: Options = { @@ -48,5 +25,4 @@ export const defaults: Options = { thresholds: [], }, ], - pageSize: 100, }; From ca5d7c3510964347522f399da9b21671b94cc19d Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 17:08:22 -0800 Subject: [PATCH 16/31] fix type errors --- packages/grafana-ui/src/components/DataTable/DataTable.tsx | 3 ++- packages/grafana-ui/src/components/DataTable/renderer.test.ts | 1 - packages/grafana-ui/src/components/DataTable/renderer.tsx | 4 ++-- packages/grafana-ui/src/utils/processTimeSeries.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/components/DataTable/DataTable.tsx b/packages/grafana-ui/src/components/DataTable/DataTable.tsx index a14522fd10f..322b3847c7b 100644 --- a/packages/grafana-ui/src/components/DataTable/DataTable.tsx +++ b/packages/grafana-ui/src/components/DataTable/DataTable.tsx @@ -86,7 +86,8 @@ export class DataTable extends Component { // Update the data when data or sort changes if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { - this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') }); + const sorted = data ? sortTableData(data, sortBy, sortDirection === 'DESC') : data; + this.setState({ data: sorted }); } } diff --git a/packages/grafana-ui/src/components/DataTable/renderer.test.ts b/packages/grafana-ui/src/components/DataTable/renderer.test.ts index fdb7dc49e4d..28f217b11f0 100644 --- a/packages/grafana-ui/src/components/DataTable/renderer.test.ts +++ b/packages/grafana-ui/src/components/DataTable/renderer.test.ts @@ -3,7 +3,6 @@ import TableModel from 'app/core/table_model'; import { getColorDefinitionByName } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui/src/types'; -import moment from 'moment'; import { TableRenderer } from './renderer'; import { Index } from 'react-virtualized'; import { ColumnStyle } from './DataTable'; diff --git a/packages/grafana-ui/src/components/DataTable/renderer.tsx b/packages/grafana-ui/src/components/DataTable/renderer.tsx index fb6263fd4d7..11977dc2e6f 100644 --- a/packages/grafana-ui/src/components/DataTable/renderer.tsx +++ b/packages/grafana-ui/src/components/DataTable/renderer.tsx @@ -98,7 +98,7 @@ export class TableRenderer { } } - createColumnFormatter(header: Column, style?: ColumnStyle): CellFormatter { + createColumnFormatter(schema: Column, style?: ColumnStyle): CellFormatter { if (!style) { return this.defaultCellFormatter; } @@ -181,7 +181,7 @@ export class TableRenderer { } if (style.type === 'number') { - const valueFormatter = getValueFormat(style.unit || header.unit); + const valueFormatter = getValueFormat(style.unit || schema.unit || 'none'); return v => { if (v === null || v === void 0) { diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 08872648f44..3f2fded1da6 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -174,8 +174,8 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS return vmSeries; } -export function sortTableData(data?: TableData, sortIndex?: number, reverse = false): TableData { - if (data && isNumber(sortIndex)) { +export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData { + if (isNumber(sortIndex)) { const copy = { ...data, rows: [...data.rows].sort((a, b) => { From e1324289c8e5f910d83d8fae72bcb49f8802e80c Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 17:49:02 -0800 Subject: [PATCH 17/31] rename to Table --- .../src/components/DataTable/DataTable.tsx | 187 ---------------- .../renderer.test.ts => Table/Table.test.ts} | 15 +- .../renderer.tsx => Table/Table.tsx} | 207 +++++++++++++++--- .../src/components/Table/_Table.scss | 0 public/app/plugins/panel/table/renderer.ts | 2 +- .../app/plugins/panel/table2/TablePanel.tsx | 5 +- public/app/plugins/panel/table2/types.ts | 2 +- 7 files changed, 194 insertions(+), 224 deletions(-) delete mode 100644 packages/grafana-ui/src/components/DataTable/DataTable.tsx rename packages/grafana-ui/src/components/{DataTable/renderer.test.ts => Table/Table.test.ts} (97%) rename packages/grafana-ui/src/components/{DataTable/renderer.tsx => Table/Table.tsx} (60%) rename public/sass/components/_panel_table2.scss => packages/grafana-ui/src/components/Table/_Table.scss (100%) diff --git a/packages/grafana-ui/src/components/DataTable/DataTable.tsx b/packages/grafana-ui/src/components/DataTable/DataTable.tsx deleted file mode 100644 index 322b3847c7b..00000000000 --- a/packages/grafana-ui/src/components/DataTable/DataTable.tsx +++ /dev/null @@ -1,187 +0,0 @@ -// Libraries -import React, { Component, ReactNode } from 'react'; -import { - Table, - SortDirectionType, - SortIndicator, - Column, - TableHeaderProps, - TableCellProps, - Index, -} from 'react-virtualized'; -import { Themeable } from '../../types/theme'; - -import { sortTableData } from '../../utils/processTimeSeries'; - -// Types -import { TableData, InterpolateFunction } from '../../types/index'; -import { TableRenderer } from './renderer'; - -// Made to match the existing (untyped) settings in the angular table -export interface ColumnStyle { - pattern?: string; - - alias?: string; - colorMode?: string; - colors?: any[]; - decimals?: number; - 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; -} - -interface Props extends Themeable { - data?: TableData; - showHeader: boolean; - styles: ColumnStyle[]; - replaceVariables: InterpolateFunction; - width: number; - height: number; -} - -interface State { - sortBy?: number; - sortDirection?: SortDirectionType; - data?: TableData; -} - -export class DataTable extends Component { - renderer: TableRenderer; - - static defaultProps = { - showHeader: true, - }; - - constructor(props: Props) { - super(props); - - this.state = { - data: props.data, - }; - - this.renderer = this.createRenderer(); - } - - 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.renderer = this.createRenderer(); - } - - // 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 }); - } - } - - // styles: ColumnStyle[], - // schema: Column[], - // rowGetter: (info: Index) => any[], // matches the table rowGetter - // replaceVariables: InterpolateFunction, - // isUTC?: boolean, // TODO? get UTC from props? - // theme?: GrafanaThemeType | undefined, - - createRenderer(): TableRenderer { - const { styles, replaceVariables, theme } = this.props; - const { data } = this.state; - - return new TableRenderer({ - styles, - schema: data ? data.columns : [], - rowGetter: this.rowGetter, - replaceVariables, - isUTC: false, - theme: theme.type, - }); - } - - 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 ( -
- {col.text} {sortBy === dataKey && } -
- ); - }; - - cellRenderer = (cell: TableCellProps) => { - const { columnIndex, rowIndex } = cell; - const row = this.state.data!.rows[rowIndex]; - const val = row[columnIndex]; - return this.renderer.renderCell(columnIndex, rowIndex, val); - }; - - render() { - const { width, height, showHeader } = this.props; - const { data } = this.props; - if (!data) { - return
NO Data
; - } - return ( - - {data.columns.map((col, index) => { - return ( - - ); - })} -
- ); - } -} - -export default DataTable; diff --git a/packages/grafana-ui/src/components/DataTable/renderer.test.ts b/packages/grafana-ui/src/components/Table/Table.test.ts similarity index 97% rename from packages/grafana-ui/src/components/DataTable/renderer.test.ts rename to packages/grafana-ui/src/components/Table/Table.test.ts index 28f217b11f0..deb34385b1a 100644 --- a/packages/grafana-ui/src/components/DataTable/renderer.test.ts +++ b/packages/grafana-ui/src/components/Table/Table.test.ts @@ -3,9 +3,8 @@ import TableModel from 'app/core/table_model'; import { getColorDefinitionByName } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui/src/types'; -import { TableRenderer } from './renderer'; -import { Index } from 'react-virtualized'; -import { ColumnStyle } from './DataTable'; +import { getTheme } from '../../themes'; +import Table, { ColumnStyle } from './Table'; // TODO: this is commented out with *x* describe! // Essentially all the elements need to replace the with
@@ -181,12 +180,14 @@ xdescribe('when rendering table', () => { } return value; }; - const rowGetter = ({ index }: Index) => table.rows[index]; - const renderer = new TableRenderer({ + const renderer = new Table({ styles, - schema: table.columns, - rowGetter, + data: table, replaceVariables, + showHeader: true, + width: 100, + height: 100, + theme: getTheme(), }); it('time column should be formated', () => { diff --git a/packages/grafana-ui/src/components/DataTable/renderer.tsx b/packages/grafana-ui/src/components/Table/Table.tsx similarity index 60% rename from packages/grafana-ui/src/components/DataTable/renderer.tsx rename to packages/grafana-ui/src/components/Table/Table.tsx index 11977dc2e6f..294be5ec321 100644 --- a/packages/grafana-ui/src/components/DataTable/renderer.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -1,17 +1,55 @@ // Libraries import _ from 'lodash'; -import moment from 'moment'; -import React, { CSSProperties, ReactNode } from 'react'; +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 { sanitize } from 'app/core/utils/text'; +import moment from 'moment'; + +import { getValueFormat, TableData, getColorFromHexRgbOrName, InterpolateFunction, Column } from '@grafana/ui'; +import { Index } from 'react-virtualized'; +import { ColumnStyle } from './Table'; + // Types import kbn from 'app/core/utils/kbn'; -import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType, InterpolateFunction, Column } from '@grafana/ui'; -import { Index } from 'react-virtualized'; -import { ColumnStyle } from './DataTable'; -type CellFormatter = (v: any, style?: ColumnStyle) => string | undefined; +// Made to match the existing (untyped) settings in the angular table +export interface ColumnStyle { + pattern?: string; + + alias?: string; + colorMode?: string; + colors?: any[]; + decimals?: number; + 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; +} + +type CellFormatter = (v: any, style?: ColumnStyle) => ReactNode; interface ColumnInfo { header: string; @@ -22,29 +60,66 @@ interface ColumnInfo { filterable?: boolean; } -interface RendererOptions { +interface Props extends Themeable { + data?: TableData; + showHeader: boolean; styles: ColumnStyle[]; - schema: Column[]; - rowGetter: (info: Index) => any[]; // matches the table rowGetter replaceVariables: InterpolateFunction; - isUTC?: boolean; // TODO? get UTC from props? - theme?: GrafanaThemeType | undefined; + width: number; + height: number; + isUTC?: boolean; } -export class TableRenderer { - columns: ColumnInfo[]; +interface State { + sortBy?: number; + sortDirection?: SortDirectionType; + data?: TableData; +} + +export class Table extends Component { + columns: ColumnInfo[] = []; colorState: any; - constructor(private options: RendererOptions) { - const { schema, styles } = options; - this.colorState = {}; + static defaultProps = { + showHeader: true, + }; - if (!schema) { + 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 = options.schema.map((col, index) => { + this.columns = data.columns.map((col, index) => { let title = col.text; let style; // ColumnStyle @@ -70,16 +145,22 @@ export class TableRenderer { }); } + //---------------------------------------------------------------------- + // renderer.ts copy (taken from angular version!!!) + //---------------------------------------------------------------------- + getColorForValue(value: any, style: ColumnStyle) { - if (!style.thresholds) { + 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], this.options.theme); + return getColorFromHexRgbOrName(style.colors[i], theme.type); } } - return getColorFromHexRgbOrName(_.first(style.colors), this.options.theme); + return getColorFromHexRgbOrName(_.first(style.colors), theme.type); } defaultCellFormatter(v: any, style?: ColumnStyle): string { @@ -119,7 +200,7 @@ export class TableRenderer { v = v[0]; } let date = moment(v); - if (this.options.isUTC) { + if (this.props.isUTC) { date = date.utc(); } return date.format(style.dateFormat); @@ -220,7 +301,7 @@ export class TableRenderer { renderRowVariables(rowIndex: number) { const scopedVars: any = {}; - const row = this.options.rowGetter({ index: rowIndex }); + const row = this.rowGetter({ index: rowIndex }); for (let i = 0; i < row.length; i++) { scopedVars[`__cell_${i}`] = { value: row[i] }; } @@ -260,7 +341,7 @@ export class TableRenderer { let columnHtml: JSX.Element; if (column.style && column.style.link) { // Render cell as link - const { replaceVariables } = this.options; + const { replaceVariables } = this.props; const scopedVars = this.renderRowVariables(rowIndex); scopedVars['__cell'] = { value: value }; @@ -329,4 +410,80 @@ export class TableRenderer { ); 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 ( +
+ {col.text} {sortBy === dataKey && } +
+ ); + }; + + 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
NO Data
; + } + return ( + + {data.columns.map((col, index) => { + return ( + + ); + })} + + ); + } } + +export default Table; diff --git a/public/sass/components/_panel_table2.scss b/packages/grafana-ui/src/components/Table/_Table.scss similarity index 100% rename from public/sass/components/_panel_table2.scss rename to packages/grafana-ui/src/components/Table/_Table.scss diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index ffb8f89b972..db6f87cfc74 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/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/DataTable/DataTable'; +import { ColumnStyle } from '@grafana/ui/src/components/Table/Table'; export class TableRenderer { formatters: any[]; diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index de4648f2c8b..a7cd84f8ecb 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -1,11 +1,10 @@ // Libraries -import _ from 'lodash'; import React, { Component } from 'react'; // Types import { PanelProps, ThemeContext } from '@grafana/ui'; import { Options } from './types'; -import DataTable from '@grafana/ui/src/components/DataTable/DataTable'; +import Table from '@grafana/ui/src/components/Table/Table'; interface Props extends PanelProps {} @@ -23,7 +22,7 @@ export class TablePanel extends Component { return ( - {theme => } + {theme => } ); } diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index 0935a0878c9..87643648f91 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/public/app/plugins/panel/table2/types.ts @@ -1,4 +1,4 @@ -import { ColumnStyle } from '@grafana/ui/src/components/DataTable/DataTable'; +import { ColumnStyle } from '@grafana/ui/src/components/Table/Table'; export interface Options { showHeader: boolean; From ad51b069b332b674404fe5b0bc0c0f1c681750ba Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 17:51:45 -0800 Subject: [PATCH 18/31] rename to Table --- public/sass/_grafana.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 5104feac48e..8928523f2be 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -57,7 +57,6 @@ @import 'components/panel_pluginlist'; @import 'components/panel_singlestat'; @import 'components/panel_table'; -@import 'components/panel_table2'; @import 'components/panel_text'; @import 'components/panel_heatmap'; @import 'components/panel_logs'; From eed9a2010e7f407c47b45ceb2876c2748ad863e4 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 17:55:23 -0800 Subject: [PATCH 19/31] fix scss --- packages/grafana-ui/src/components/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index a475a7ccc1b..f134f73f745 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -1,6 +1,7 @@ @import 'CustomScrollbar/CustomScrollbar'; @import 'DeleteButton/DeleteButton'; @import 'ThresholdsEditor/ThresholdsEditor'; +@import 'Table/Table'; @import 'Tooltip/Tooltip'; @import 'Select/Select'; @import 'PanelOptionsGroup/PanelOptionsGroup'; From 223906654cc02486762d05dc7f77235012192064 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 18:27:51 -0800 Subject: [PATCH 20/31] don't include stuff from app/... --- .../src/components/Table/Table.test.ts | 41 +++++++++---------- .../grafana-ui/src/components/Table/Table.tsx | 16 +++----- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.test.ts b/packages/grafana-ui/src/components/Table/Table.test.ts index deb34385b1a..0e08473f149 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.ts +++ b/packages/grafana-ui/src/components/Table/Table.test.ts @@ -1,8 +1,7 @@ import _ from 'lodash'; -import TableModel from 'app/core/table_model'; import { getColorDefinitionByName } from '@grafana/ui'; -import { ScopedVars } from '@grafana/ui/src/types'; +import { ScopedVars, TableData } from '@grafana/ui/src/types'; import { getTheme } from '../../themes'; import Table, { ColumnStyle } from './Table'; @@ -12,25 +11,25 @@ xdescribe('when rendering table', () => { const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange'); describe('given 13 columns', () => { - const table = new TableModel(); - table.columns = [ - { text: 'Time' }, - { text: 'Value' }, - { text: 'Colored' }, - { text: 'Undefined' }, - { text: 'String' }, - { text: 'United', unit: 'bps' }, - { text: 'Sanitized' }, - { text: 'Link' }, - { text: 'Array' }, - { text: 'Mapping' }, - { text: 'RangeMapping' }, - { text: 'MappingColored' }, - { text: 'RangeMappingColored' }, - ]; - table.rows = [ - [1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2], - ]; + const table = { + type: 'table', + columns: [ + { text: 'Time' }, + { text: 'Value' }, + { text: 'Colored' }, + { text: 'Undefined' }, + { text: 'String' }, + { text: 'United', unit: 'bps' }, + { text: 'Sanitized' }, + { text: 'Link' }, + { text: 'Array' }, + { text: 'Mapping' }, + { text: 'RangeMapping' }, + { text: 'MappingColored' }, + { text: 'RangeMappingColored' }, + ], + rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]], + } as TableData; const styles: ColumnStyle[] = [ { diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 294be5ec321..63ff8e55a6a 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -13,16 +13,14 @@ import { Themeable } from '../../types/theme'; import { sortTableData } from '../../utils/processTimeSeries'; -import { sanitize } from 'app/core/utils/text'; - import moment from 'moment'; import { getValueFormat, TableData, getColorFromHexRgbOrName, InterpolateFunction, Column } from '@grafana/ui'; import { Index } from 'react-virtualized'; import { ColumnStyle } from './Table'; -// Types -import kbn from 'app/core/utils/kbn'; +// APP Imports!!! +// import kbn from 'app/core/utils/kbn'; // Made to match the existing (untyped) settings in the angular table export interface ColumnStyle { @@ -36,7 +34,7 @@ export interface ColumnStyle { type?: 'date' | 'number' | 'string' | 'hidden'; unit?: string; dateFormat?: string; - sanitize?: boolean; + sanitize?: boolean; // not used in react mappingType?: any; valueMaps?: any; rangeMaps?: any; @@ -126,7 +124,7 @@ export class Table extends Component { // Find the style based on the text for (let i = 0; i < styles.length; i++) { const s = styles[i]; - const regex = kbn.stringToJsRegex(s.pattern); + const regex = 'XXX'; //kbn.stringToJsRegex(s.pattern); if (title.match(regex)) { style = s; if (s.alias) { @@ -172,11 +170,7 @@ export class Table extends Component { v = v.join(', '); } - if (style && style.sanitize) { - return sanitize(v); - } else { - return _.escape(v); - } + return v; // react will sanitize } createColumnFormatter(schema: Column, style?: ColumnStyle): CellFormatter { From 5aef3bd1395d515c4f5740e0775ce9f215b97694 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 8 Mar 2019 00:28:42 -0800 Subject: [PATCH 21/31] add storybook --- .../src/components/Table/Table.story.tsx | 70 ++++++++ .../src/components/Table/Table.test.ts | 167 +----------------- .../grafana-ui/src/components/Table/Table.tsx | 1 + .../src/components/Table/examples.ts | 167 ++++++++++++++++++ 4 files changed, 246 insertions(+), 159 deletions(-) create mode 100644 packages/grafana-ui/src/components/Table/Table.story.tsx create mode 100644 packages/grafana-ui/src/components/Table/examples.ts diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx new file mode 100644 index 00000000000..a494f01c56d --- /dev/null +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -0,0 +1,70 @@ +import React, { FunctionComponent } from 'react'; +import { storiesOf } from '@storybook/react'; +import { Table } from './Table'; + +import { migratedTestTable, migratedTestStyles, simpleTable } from './examples'; +import { ScopedVars } from '../../types/index'; + +import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; +import { AutoSizer } from 'react-virtualized'; + +const CenteredStory: FunctionComponent<{}> = ({ children }) => { + return ( +
+ + {({ width, height }) => ( +
+
+ Need to pass {width}/{height} to the table? +
+ {children} +
+ )} +
+
+ ); +}; + +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; +}; + +storiesOf('UI - Alpha/Table', module) + .addDecorator(story => {story()}) + .add('basic', () => { + return renderComponentWithTheme(Table, { + styles: [], + data: simpleTable, + replaceVariables, + showHeader: true, + width: 500, + height: 300, + }); + }) + .add('Test Configuration', () => { + return renderComponentWithTheme(Table, { + styles: migratedTestStyles, + data: migratedTestTable, + replaceVariables, + showHeader: true, + width: 500, + height: 300, + }); + }); diff --git a/packages/grafana-ui/src/components/Table/Table.test.ts b/packages/grafana-ui/src/components/Table/Table.test.ts index 0e08473f149..93cf53b2adb 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.ts +++ b/packages/grafana-ui/src/components/Table/Table.test.ts @@ -1,9 +1,11 @@ import _ from 'lodash'; import { getColorDefinitionByName } from '@grafana/ui'; -import { ScopedVars, TableData } from '@grafana/ui/src/types'; +import { ScopedVars } from '@grafana/ui/src/types'; import { getTheme } from '../../themes'; -import Table, { ColumnStyle } from './Table'; +import Table from './Table'; + +import { migratedTestTable, migratedTestStyles } from './examples'; // TODO: this is commented out with *x* describe! // Essentially all the elements need to replace the
'); - }); - - it('time column with epoch as string should be formatted', () => { - const html = renderer.renderCell(0, 0, '1388556366666'); - expect(html).toBe(''); - }); - - 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(''); - }); - - 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(''); - }); - - it('undefined time column should be rendered as -', () => { - const html = renderer.renderCell(0, 0, undefined); - expect(html).toBe(''); - }); - - it('null time column should be rendered as -', () => { - const html = renderer.renderCell(0, 0, null); - expect(html).toBe(''); - }); - - it('number column with unit specified should ignore style unit', () => { - const html = renderer.renderCell(5, 0, 1230); - expect(html).toBe(''); - }); - - it('number column should be formated', () => { - const html = renderer.renderCell(1, 0, 1230); - expect(html).toBe(''); - }); - - it('number style should ignore string values', () => { - const html = renderer.renderCell(1, 0, 'asd'); - expect(html).toBe(''); - }); - - it('colored cell should have style (handles HEX color values)', () => { - const html = renderer.renderCell(2, 0, 40); - expect(html).toBe(''); - }); - - it('colored cell should have style (handles named color values', () => { - const html = renderer.renderCell(2, 0, 55); - expect(html).toBe(``); - }); - - it('colored cell should have style handles(rgb color values)', () => { - const html = renderer.renderCell(2, 0, 85); - expect(html).toBe(''); - }); - - it('unformated undefined should be rendered as string', () => { - const html = renderer.renderCell(3, 0, 'value'); - expect(html).toBe(''); - }); - - it('string style with escape html should return escaped html', () => { - const html = renderer.renderCell(4, 0, '&breaking
the
row'); - expect(html).toBe(''); - }); - - it('undefined formater should return escaped html', () => { - const html = renderer.renderCell(3, 0, '&breaking
the
row'); - expect(html).toBe(''); - }); - - it('undefined value should render as -', () => { - const html = renderer.renderCell(3, 0, undefined); - expect(html).toBe(''); - }); - - it('sanitized value should render as', () => { - const html = renderer.renderCell(6, 0, 'text link'); - expect(html).toBe(''); - }); - - 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 = ` - - `; - 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(''); - }); - - it('numeric value should be mapped to text', () => { - const html = renderer.renderCell(9, 0, 1); - expect(html).toBe(''); - }); - - it('string numeric value should be mapped to text', () => { - const html = renderer.renderCell(9, 0, '0'); - expect(html).toBe(''); - }); - - it('string value should be mapped to text', () => { - const html = renderer.renderCell(9, 0, 'HELLO WORLD'); - expect(html).toBe(''); - }); - - it('array column value should be mapped to text', () => { - const html = renderer.renderCell(9, 0, ['value1', 'value2']); - expect(html).toBe(''); - }); - - it('value should be mapped to text (range)', () => { - const html = renderer.renderCell(10, 0, 2); - expect(html).toBe(''); - }); - - it('value should be mapped to text (range)', () => { - const html = renderer.renderCell(10, 0, 5); - expect(html).toBe(''); - }); - - it('array column value should not be mapped to text', () => { - const html = renderer.renderCell(10, 0, ['value1', 'value2']); - expect(html).toBe(''); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, 1); - expect(html).toBe(``); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, '1'); - expect(html).toBe(``); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, 0); - expect(html).toBe(''); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, '0'); - expect(html).toBe(''); - }); - - it('value should be mapped to text and colored cell should have style', () => { - const html = renderer.renderCell(11, 0, '2.1'); - expect(html).toBe(''); - }); - - it('value should be mapped to text (range) and colored cell should have style', () => { - const html = renderer.renderCell(12, 0, 0); - expect(html).toBe(''); - }); - - it('value should be mapped to text (range) and colored cell should have style', () => { - const html = renderer.renderCell(12, 0, 1); - expect(html).toBe(''); - }); - - it('value should be mapped to text (range) and colored cell should have style', () => { - const html = renderer.renderCell(12, 0, 4); - expect(html).toBe(``); - }); - - 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(''); - }); - }); -}); - -function normalize(str: string) { - return str.replace(/\s+/gm, ' ').trim(); -} diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index cbc0b50ebc0..2a75c33224e 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/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 { - columns: ColumnInfo[] = []; - colorState: any; - - _cache: CellMeasurerCache; + columns: ColumnInfo[]; + measurer: CellMeasurerCache; static defaultProps = { showHeader: true, @@ -92,12 +57,11 @@ export class Table extends Component { 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 { 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 { } } - 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 { 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 { } }; - 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 ( -
- {col.text}{' '} +
this.handleCellClick(rowIndex, columnIndex)}> + {col.text} {sorting && ( {sortDirection} @@ -168,43 +164,22 @@ export class Table extends Component { }; 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
?
; + return
??
; } 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 = ( -
- {rowIndex}/{columnIndex}: {value} -
- ); - } + 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 ( - -
this.handelClick(rowIndex, columnIndex)} - className={classNames} - style={{ - ...style, - whiteSpace: 'nowrap', - }} - > - {content} -
+ + {builder({ value, row, table: this, props })} ); }; @@ -218,16 +193,16 @@ export class Table extends Component { return ( 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 ( +
+ {value} +
+ ); +}; + +// *************************************************************************** +// 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 = ( + + {value} + + ); + } + + // ??? I don't think this will still work! + if (this.column.filterable) { + cellClasses.push('table-panel-cell-filterable'); + value = ( + <> + {value} + + + + + + + + + + ); + } + + let className; + if (cellClasses.length) { + className = cellClasses.join(' '); + } + + return simpleCellBuilder({ value, props, className }); + }; +} diff --git a/packages/grafana-ui/src/components/Table/TableXXXX.tsx b/packages/grafana-ui/src/components/Table/TableXXXX.tsx deleted file mode 100644 index 4c78c3b336b..00000000000 --- a/packages/grafana-ui/src/components/Table/TableXXXX.tsx +++ /dev/null @@ -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 { - 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 = ( - - {value} - - ); - } else { - columnHtml = {value}; - } - - let filterLink: JSX.Element | null = null; - if (column.filterable) { - cellClasses.push('table-panel-cell-filterable'); - filterLink = ( - - - - - - - - - ); - } - - if (cellClasses.length) { - cellClass = cellClasses.join(' '); - } - - style.width = '100%'; - style.height = '100%'; - columnHtml = ( -
- {columnHtml} - {filterLink} -
- ); - 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 ( -
- {col.text} {sortBy === dataKey && } -
- ); - }; - - 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
NO Data
; - } - - return ( - - {data.columns.map((col, index) => { - return ( - - ); - })} - - ); - } -} - -export default TableXXXX; diff --git a/packages/grafana-ui/src/components/Table/_Table.scss b/packages/grafana-ui/src/components/Table/_Table.scss index f9cb0271561..22170ecf913 100644 --- a/packages/grafana-ui/src/components/Table/_Table.scss +++ b/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; } diff --git a/packages/grafana-ui/src/components/Table/examples.ts b/packages/grafana-ui/src/components/Table/examples.ts index 026c5446f6b..9f05488d839 100644 --- a/packages/grafana-ui/src/components/Table/examples.ts +++ b/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'; diff --git a/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx b/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx index c6efbee462f..98c3ccb2e7f 100644 --- a/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx +++ b/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, props: any) => (
Date: Sat, 9 Mar 2019 23:07:42 -0800 Subject: [PATCH 24/31] add variable size storybook --- .../src/components/Table/Table.story.tsx | 72 ++++++++++--------- .../grafana-ui/src/components/Table/Table.tsx | 57 +++++++++------ .../src/components/Table/TableCellBuilder.tsx | 2 +- .../src/components/Table/_Table.scss | 5 +- 4 files changed, 80 insertions(+), 56 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 1aa618d73d7..240c7def7f3 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -18,11 +18,28 @@ const replaceVariables = (value: string, scopedVars?: ScopedVars) => { return value; }; -storiesOf('UI/Table', module) +export function makeDummyTable(columnCount: number, rowCount: number): TableData { + const A = 'A'.charCodeAt(0); + return { + columns: Array.from(new Array(columnCount), (x, i) => { + return { + text: String.fromCharCode(A + i), + }; + }), + rows: Array.from(new Array(rowCount), (x, rowId) => { + const suffix = (rowId + 1).toString(); + return Array.from(new Array(columnCount), (x, colId) => String.fromCharCode(A + colId) + suffix); + }), + type: 'table', + columnMap: {}, + }; +} + +storiesOf('Alpha/Table', module) .add('basic', () => { const showHeader = boolean('Show Header', true); - const fixedRowCount = number('Fixed Rows', 1); - const fixedColumnCount = number('Fixed Columns', 1); + const fixedRowCount = number('Fixed Rows', 1, { min: 0, max: 50, step: 1, range: false }); + const fixedColumnCount = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); return withFullSizeStory(Table, { styles: [], @@ -33,41 +50,28 @@ storiesOf('UI/Table', module) showHeader, }); }) - .add('Test Configuration', () => { + .add('variable size', () => { + const columnCount = number('Column Count', 10, { min: 2, max: 50, step: 1, range: false }); + const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false }); + + const showHeader = boolean('Show Header', true); + const fixedRowCount = number('Fixed Rows', 1, { min: 0, max: 50, step: 1, range: false }); + const fixedColumnCount = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); + + return withFullSizeStory(Table, { + styles: [], + data: makeDummyTable(columnCount, rowCount), + replaceVariables, + fixedRowCount, + fixedColumnCount, + showHeader, + }); + }) + .add('Old tests configuration', () => { return withFullSizeStory(Table, { styles: migratedTestStyles, data: migratedTestTable, replaceVariables, showHeader: true, }); - }) - .add('Lots of cells', () => { - const data = { - columns: [], - rows: [], - type: 'table', - columnMap: {}, - } as TableData; - for (let i = 0; i < 20; i++) { - data.columns.push({ - text: 'Column ' + i, - }); - } - for (let r = 0; r < 500; r++) { - const row = []; - for (let i = 0; i < 20; i++) { - row.push(r + i); - } - data.rows.push(row); - } - console.log('DATA:', data); - - return withFullSizeStory(Table, { - styles: simpleTable, - data, - replaceVariables, - showHeader: true, - fixedColumnCount: 1, - fixedRowCount: 1, - }); }); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 2a75c33224e..509eaf5d676 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -43,6 +43,7 @@ interface State { export class Table extends Component { columns: ColumnInfo[]; measurer: CellMeasurerCache; + scrollToTop = false; static defaultProps = { showHeader: true, @@ -65,12 +66,13 @@ export class Table extends Component { } componentDidUpdate(prevProps: Props, prevState: State) { - const { data, styles } = this.props; + const { data, styles, showHeader } = this.props; const { sortBy, sortDirection } = this.state; const dataChanged = data !== prevProps.data; + const configsChanged = showHeader !== prevProps.showHeader; // Reset the size cache - if (dataChanged) { + if (dataChanged || configsChanged) { this.measurer.clearAll(); } @@ -81,8 +83,8 @@ export class Table extends Component { // 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 }); + this.scrollToTop = true; + this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') }); } } @@ -137,7 +139,7 @@ export class Table extends Component { if (realRowIndex < 0) { this.doSort(columnIndex); } else { - const row = data!.rows[realRowIndex]; + const row = data.rows[realRowIndex]; const value = row[columnIndex]; console.log('CLICK', rowIndex, columnIndex, value); } @@ -147,18 +149,19 @@ export class Table extends Component { const { data, sortBy, sortDirection } = this.state; const { columnIndex, rowIndex, style } = cell.props; - const col = data!.columns[columnIndex]; + let col = data.columns[columnIndex]; const sorting = sortBy === columnIndex; + if (!col) { + // NOT SURE Why this happens sometimes + col = { + text: '??' + columnIndex + '???', + }; + } return (
this.handleCellClick(rowIndex, columnIndex)}> {col.text} - {sorting && ( - - {sortDirection} - - - )} + {sorting && }
); }; @@ -167,15 +170,16 @@ export class Table extends Component { const { rowIndex, columnIndex, key, parent } = props; const { showHeader } = this.props; const { data } = this.state; - if (!data) { - return
??
; + const column = this.columns[columnIndex]; + if (!column) { + return
XXX
; // NOT SURE HOW/WHY THIS HAPPENS! } const realRowIndex = rowIndex - (showHeader ? 1 : 0); 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; + const builder = isHeader ? this.headerBuilder : column.builder; return ( @@ -185,9 +189,18 @@ export class Table extends Component { }; render() { - const { data, showHeader, width, height, fixedColumnCount, fixedRowCount } = this.props; - if (!data) { - return
NO Data
; + const { data, showHeader, width, height } = this.props; + + const columnCount = data.columns.length; + const rowCount = data.rows.length + (showHeader ? 1 : 0); + + const fixedColumnCount = Math.min(this.props.fixedColumnCount, columnCount); + const fixedRowCount = Math.min(this.props.fixedRowCount, rowCount); + + // Usually called after a sort or the data changes + const scrollToRow = this.scrollToTop ? 1 : -1; + if (this.scrollToTop) { + this.scrollToTop = false; } return ( @@ -195,8 +208,12 @@ export class Table extends Component { { ...this.state /** Force MultiGrid to update when data changes */ } - columnCount={data.columns.length} - rowCount={data.rows.length + (showHeader ? 1 : 0)} + { + ...this.props /** Force MultiGrid to update when data changes */ + } + scrollToRow={scrollToRow} + columnCount={columnCount} + rowCount={rowCount} overscanColumnCount={2} overscanRowCount={2} columnWidth={this.measurer.columnWidth} diff --git a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx index d8816b08f2e..2f143c81e3b 100644 --- a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx @@ -25,7 +25,7 @@ export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOption const { style } = props; return ( -
+
{value}
); diff --git a/packages/grafana-ui/src/components/Table/_Table.scss b/packages/grafana-ui/src/components/Table/_Table.scss index 22170ecf913..05569f84e2e 100644 --- a/packages/grafana-ui/src/components/Table/_Table.scss +++ b/packages/grafana-ui/src/components/Table/_Table.scss @@ -41,7 +41,6 @@ } .ReactVirtualized__Table__sortableHeaderIconContainer { - display: flex; align-items: center; } .ReactVirtualized__Table__sortableHeaderIcon { @@ -73,3 +72,7 @@ border-right: 2px solid $body-bg; border-bottom: 2px solid $body-bg; } + +.gf-table-fixed-column { + border-right: 1px solid #ccc; +} From 494acddb2f912fcd326dc4f732408fc2d27f649f Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 9 Mar 2019 23:39:11 -0800 Subject: [PATCH 25/31] get field mapping to actually work --- .../grafana-ui/src/components/Table/Table.tsx | 17 ++++++++++++++--- .../src/components/Table/TableCellBuilder.tsx | 7 +++---- packages/grafana-ui/src/utils/index.ts | 1 + packages/grafana-ui/src/utils/stringUtils.ts | 13 +++++++++++++ public/app/core/utils/kbn.ts | 15 +++------------ 5 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 packages/grafana-ui/src/utils/stringUtils.ts diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 509eaf5d676..ee0a90c4bc4 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -15,6 +15,7 @@ import { sortTableData } from '../../utils/processTimeSeries'; import { TableData, InterpolateFunction } from '@grafana/ui'; import { TableCellBuilder, ColumnStyle, getCellBuilder, TableCellBuilderOptions } from './TableCellBuilder'; +import { stringToJsRegex } from '../../utils/index'; interface ColumnInfo { index: number; @@ -90,6 +91,8 @@ export class Table extends Component { initColumns(props: Props): ColumnInfo[] { const { styles, data } = props; + console.log('STYLES', styles); + return data.columns.map((col, index) => { let title = col.text; let style: ColumnStyle | null = null; // ColumnStyle @@ -97,7 +100,7 @@ export class Table extends Component { // 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); + const regex = stringToJsRegex(s.pattern); if (title.match(regex)) { style = s; if (s.alias) { @@ -170,14 +173,22 @@ export class Table extends Component { const { rowIndex, columnIndex, key, parent } = props; const { showHeader } = this.props; const { data } = this.state; + const column = this.columns[columnIndex]; if (!column) { - return
XXX
; // NOT SURE HOW/WHY THIS HAPPENS! + // NOT SURE HOW/WHY THIS HAPPENS! + // Without it it will crash in storybook when you cycle up/down the # of columns + // this cell is never visible in the output? + return ( +
+ XXXXX +
+ ); } const realRowIndex = rowIndex - (showHeader ? 1 : 0); const isHeader = realRowIndex < 0; - const row = isHeader ? (data.columns as any[]) : data.rows[realRowIndex]; + const row = isHeader ? data.columns : data.rows[realRowIndex]; const value = row[columnIndex]; const builder = isHeader ? this.headerBuilder : column.builder; diff --git a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx index 2f143c81e3b..75668a7e042 100644 --- a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx @@ -41,12 +41,9 @@ export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOption // // *************************************************************************** -// 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; + pattern: string; alias?: string; colorMode?: 'cell' | 'value'; @@ -89,6 +86,8 @@ export function getCellBuilder(schema: Column, style: ColumnStyle | null, props: } if (style.type === 'date') { + console.log('MAKE DATE Column', schema, style); + return new CellBuilderWithStyle( (v: any) => { if (v === undefined || v === null) { diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index c5f4b2c5b1b..a5ae301710b 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -2,4 +2,5 @@ export * from './processTimeSeries'; export * from './valueFormats/valueFormats'; export * from './colors'; export * from './namedColorsPalette'; +export * from './stringUtils'; export { getMappedValue } from './valueMappings'; diff --git a/packages/grafana-ui/src/utils/stringUtils.ts b/packages/grafana-ui/src/utils/stringUtils.ts new file mode 100644 index 00000000000..12433623a6a --- /dev/null +++ b/packages/grafana-ui/src/utils/stringUtils.ts @@ -0,0 +1,13 @@ +export function stringToJsRegex(str: string): RegExp { + if (str[0] !== '/') { + return new RegExp('^' + str + '$'); + } + + const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); + + if (!match) { + throw new Error(`'${str}' is not a valid regular expression.`); + } + + return new RegExp(match[1], match[2]); +} diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 43886fafd07..2f9b564fb27 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { getValueFormat, getValueFormatterIndex, getValueFormats } from '@grafana/ui'; +import { getValueFormat, getValueFormatterIndex, getValueFormats, stringToJsRegex } from '@grafana/ui'; const kbn: any = {}; @@ -229,17 +229,8 @@ kbn.slugifyForUrl = str => { }; kbn.stringToJsRegex = str => { - if (str[0] !== '/') { - return new RegExp('^' + str + '$'); - } - - const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); - - if (!match) { - throw new Error(`'${str}' is not a valid regular expression.`); - } - - return new RegExp(match[1], match[2]); + console.warn('Use grafana/ui stringToJsRegex'); + return stringToJsRegex(str); }; kbn.toFixed = (value, decimals) => { From b80c773ebe0f1b30f150c8b84dccab25531b88d1 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 10 Mar 2019 15:34:02 -0700 Subject: [PATCH 26/31] attach themes to table story --- .../grafana-ui/src/components/Table/Table.story.tsx | 8 ++++++-- packages/grafana-ui/src/components/Table/Table.tsx | 2 ++ .../src/components/Table/TableCellBuilder.tsx | 3 +-- packages/grafana-ui/src/utils/stringUtils.ts | 13 ------------- 4 files changed, 9 insertions(+), 17 deletions(-) delete mode 100644 packages/grafana-ui/src/utils/stringUtils.ts diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 240c7def7f3..a9a32eb29f4 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -1,9 +1,10 @@ // import React from 'react'; import { storiesOf } from '@storybook/react'; import { Table } from './Table'; +import { getTheme } from '../../themes'; import { migratedTestTable, migratedTestStyles, simpleTable } from './examples'; -import { ScopedVars, TableData } from '../../types/index'; +import { ScopedVars, TableData, GrafanaThemeType } from '../../types/index'; import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory'; import { number, boolean } from '@storybook/addon-knobs'; @@ -48,10 +49,11 @@ storiesOf('Alpha/Table', module) fixedRowCount, fixedColumnCount, showHeader, + theme: getTheme(GrafanaThemeType.Light), }); }) .add('variable size', () => { - const columnCount = number('Column Count', 10, { min: 2, max: 50, step: 1, range: false }); + const columnCount = number('Column Count', 20, { min: 2, max: 50, step: 1, range: false }); const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false }); const showHeader = boolean('Show Header', true); @@ -65,6 +67,7 @@ storiesOf('Alpha/Table', module) fixedRowCount, fixedColumnCount, showHeader, + theme: getTheme(GrafanaThemeType.Light), }); }) .add('Old tests configuration', () => { @@ -73,5 +76,6 @@ storiesOf('Alpha/Table', module) data: migratedTestTable, replaceVariables, showHeader: true, + theme: getTheme(GrafanaThemeType.Light), }); }); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index ee0a90c4bc4..8f2e3d7058b 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -72,6 +72,8 @@ export class Table extends Component { const dataChanged = data !== prevProps.data; const configsChanged = showHeader !== prevProps.showHeader; + console.log('TABLE', this.props.theme); + // Reset the size cache if (dataChanged || configsChanged) { this.measurer.clearAll(); diff --git a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx index 75668a7e042..9e0a111aba1 100644 --- a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx @@ -86,8 +86,6 @@ export function getCellBuilder(schema: Column, style: ColumnStyle | null, props: } if (style.type === 'date') { - console.log('MAKE DATE Column', schema, style); - return new CellBuilderWithStyle( (v: any) => { if (v === undefined || v === null) { @@ -160,6 +158,7 @@ class CellBuilderWithStyle { private fmt?: ValueFormatter ) { // + console.log('COLUMN', column.text, theme); } getColorForValue = (value: any): string | null => { diff --git a/packages/grafana-ui/src/utils/stringUtils.ts b/packages/grafana-ui/src/utils/stringUtils.ts deleted file mode 100644 index 12433623a6a..00000000000 --- a/packages/grafana-ui/src/utils/stringUtils.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function stringToJsRegex(str: string): RegExp { - if (str[0] !== '/') { - return new RegExp('^' + str + '$'); - } - - const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); - - if (!match) { - throw new Error(`'${str}' is not a valid regular expression.`); - } - - return new RegExp(match[1], match[2]); -} From 91a2307b9881cb72f646c5b92b4ee5f57f36964e Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 10 Mar 2019 23:11:57 -0700 Subject: [PATCH 27/31] rotate! --- .../src/components/Table/Table.story.tsx | 48 ++++-- .../grafana-ui/src/components/Table/Table.tsx | 160 +++++++++++------- .../src/components/Table/TableCellBuilder.tsx | 1 + .../src/components/Table/examples.ts | 4 +- .../plugins/panel/table2/TablePanelEditor.tsx | 35 +++- public/app/plugins/panel/table2/types.ts | 7 + 6 files changed, 175 insertions(+), 80 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index a9a32eb29f4..03765730343 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -19,17 +19,26 @@ const replaceVariables = (value: string, scopedVars?: ScopedVars) => { return value; }; -export function makeDummyTable(columnCount: number, rowCount: number): TableData { +export function columnIndexToLeter(column: number) { const A = 'A'.charCodeAt(0); + const c1 = Math.floor(column / 26); + const c2 = column % 26; + if (c1 > 0) { + return String.fromCharCode(A + c1 - 1) + String.fromCharCode(A + c2); + } + return String.fromCharCode(A + c2); +} + +export function makeDummyTable(columnCount: number, rowCount: number): TableData { return { columns: Array.from(new Array(columnCount), (x, i) => { return { - text: String.fromCharCode(A + i), + text: columnIndexToLeter(i), }; }), rows: Array.from(new Array(rowCount), (x, rowId) => { const suffix = (rowId + 1).toString(); - return Array.from(new Array(columnCount), (x, colId) => String.fromCharCode(A + colId) + suffix); + return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix); }), type: 'table', columnMap: {}, @@ -37,45 +46,54 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData } storiesOf('Alpha/Table', module) - .add('basic', () => { + .add('Basic Table', () => { + // NOTE: This example does not seem to survice rotate & + // Changing fixed headers... but the next one does? + // perhaps `simpleTable` is static and reused? + const showHeader = boolean('Show Header', true); - const fixedRowCount = number('Fixed Rows', 1, { min: 0, max: 50, step: 1, range: false }); - const fixedColumnCount = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); + const fixedHeader = boolean('Fixed Header', true); + const fixedColumns = number('Fixed Columns', 0, { min: 0, max: 50, step: 1, range: false }); + const rotate = boolean('Rotate', false); return withFullSizeStory(Table, { styles: [], data: simpleTable, replaceVariables, - fixedRowCount, - fixedColumnCount, showHeader, + fixedHeader, + fixedColumns, + rotate, theme: getTheme(GrafanaThemeType.Light), }); }) - .add('variable size', () => { - const columnCount = number('Column Count', 20, { min: 2, max: 50, step: 1, range: false }); + .add('Variable Size', () => { + const columnCount = number('Column Count', 15, { min: 2, max: 50, step: 1, range: false }); const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false }); const showHeader = boolean('Show Header', true); - const fixedRowCount = number('Fixed Rows', 1, { min: 0, max: 50, step: 1, range: false }); - const fixedColumnCount = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); + const fixedHeader = boolean('Fixed Header', true); + const fixedColumns = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); + const rotate = boolean('Rotate', false); return withFullSizeStory(Table, { styles: [], data: makeDummyTable(columnCount, rowCount), replaceVariables, - fixedRowCount, - fixedColumnCount, showHeader, + fixedHeader, + fixedColumns, + rotate, theme: getTheme(GrafanaThemeType.Light), }); }) - .add('Old tests configuration', () => { + .add('Test Config (migrated)', () => { return withFullSizeStory(Table, { styles: migratedTestStyles, data: migratedTestTable, replaceVariables, showHeader: true, + rotate: true, theme: getTheme(GrafanaThemeType.Light), }); }); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 8f2e3d7058b..4f7dce29f0d 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -14,21 +14,24 @@ import { Themeable } from '../../types/theme'; import { sortTableData } from '../../utils/processTimeSeries'; import { TableData, InterpolateFunction } from '@grafana/ui'; -import { TableCellBuilder, ColumnStyle, getCellBuilder, TableCellBuilderOptions } from './TableCellBuilder'; +import { + TableCellBuilder, + ColumnStyle, + getCellBuilder, + TableCellBuilderOptions, + simpleCellBuilder, +} from './TableCellBuilder'; import { stringToJsRegex } from '../../utils/index'; -interface ColumnInfo { - index: number; - header: string; - builder: TableCellBuilder; -} - export interface Props extends Themeable { data: TableData; + showHeader: boolean; - fixedColumnCount: number; - fixedRowCount: number; + fixedHeader: boolean; + fixedColumns: number; + rotate: boolean; styles: ColumnStyle[]; + replaceVariables: InterpolateFunction; width: number; height: number; @@ -41,15 +44,26 @@ interface State { data: TableData; } +interface ColumnRenderInfo { + header: string; + builder: TableCellBuilder; +} + +interface DataIndex { + column: number; + row: number; // -1 is the header! +} + export class Table extends Component { - columns: ColumnInfo[]; + renderer: ColumnRenderInfo[]; measurer: CellMeasurerCache; scrollToTop = false; static defaultProps = { showHeader: true, - fixedRowCount: 1, - fixedColumnCount: 0, + fixedHeader: true, + fixedColumns: 0, + rotate: false, }; constructor(props: Props) { @@ -59,7 +73,7 @@ export class Table extends Component { data: props.data, }; - this.columns = this.initColumns(props); + this.renderer = this.initColumns(props); this.measurer = new CellMeasurerCache({ defaultHeight: 30, defaultWidth: 150, @@ -70,9 +84,11 @@ export class Table extends Component { const { data, styles, showHeader } = this.props; const { sortBy, sortDirection } = this.state; const dataChanged = data !== prevProps.data; - const configsChanged = showHeader !== prevProps.showHeader; - - console.log('TABLE', this.props.theme); + const configsChanged = + showHeader !== prevProps.showHeader || + this.props.rotate !== prevProps.rotate || + this.props.fixedColumns !== prevProps.fixedColumns || + this.props.fixedHeader !== prevProps.fixedHeader; // Reset the size cache if (dataChanged || configsChanged) { @@ -80,8 +96,9 @@ export class Table extends Component { } // Update the renderer if options change + // We only *need* do to this if the header values changes, but this does every data update if (dataChanged || styles !== prevProps.styles) { - this.columns = this.initColumns(this.props); + this.renderer = this.initColumns(this.props); } // Update the data when data or sort changes @@ -91,9 +108,9 @@ export class Table extends Component { } } - initColumns(props: Props): ColumnInfo[] { + /** Given the configuration, setup how each column gets rendered */ + initColumns(props: Props): ColumnRenderInfo[] { const { styles, data } = props; - console.log('STYLES', styles); return data.columns.map((col, index) => { let title = col.text; @@ -113,7 +130,6 @@ export class Table extends Component { } return { - index, header: title, builder: getCellBuilder(col, style, this.props), }; @@ -137,27 +153,37 @@ export class Table extends Component { this.setState({ sortBy: sort, sortDirection: dir }); }; - handleCellClick = (rowIndex: number, columnIndex: number) => { - const { showHeader } = this.props; - const { data } = this.state; - const realRowIndex = rowIndex - (showHeader ? 1 : 0); - if (realRowIndex < 0) { - this.doSort(columnIndex); + /** Converts the grid coordinates to TableData coordinates */ + getCellRef = (rowIndex: number, columnIndex: number): DataIndex => { + const { showHeader, rotate } = this.props; + const rowOffset = showHeader ? -1 : 0; + + if (rotate) { + return { column: rowIndex, row: columnIndex + rowOffset }; } else { - const row = data.rows[realRowIndex]; - const value = row[columnIndex]; - console.log('CLICK', rowIndex, columnIndex, value); + return { column: columnIndex, row: rowIndex + rowOffset }; + } + }; + + handleCellClick = (rowIndex: number, columnIndex: number) => { + const { row, column } = this.getCellRef(rowIndex, columnIndex); + if (row < 0) { + this.doSort(column); + } else { + const values = this.state.data.rows[row]; + const value = values[column]; + console.log('CLICK', value, row); } }; headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => { const { data, sortBy, sortDirection } = this.state; const { columnIndex, rowIndex, style } = cell.props; + const { column } = this.getCellRef(rowIndex, columnIndex); - let col = data.columns[columnIndex]; - const sorting = sortBy === columnIndex; + let col = data.columns[column]; + const sorting = sortBy === column; if (!col) { - // NOT SURE Why this happens sometimes col = { text: '??' + columnIndex + '???', }; @@ -171,47 +197,60 @@ export class Table extends Component { ); }; + getTableCellBuilder = (column: number): TableCellBuilder => { + const render = this.renderer[column]; + if (render && render.builder) { + return render.builder; + } + return simpleCellBuilder; // the default + }; + cellRenderer = (props: GridCellProps): React.ReactNode => { const { rowIndex, columnIndex, key, parent } = props; - const { showHeader } = this.props; + const { row, column } = this.getCellRef(rowIndex, columnIndex); const { data } = this.state; - const column = this.columns[columnIndex]; - if (!column) { - // NOT SURE HOW/WHY THIS HAPPENS! - // Without it it will crash in storybook when you cycle up/down the # of columns - // this cell is never visible in the output? - return ( -
- XXXXX -
- ); - } - - const realRowIndex = rowIndex - (showHeader ? 1 : 0); - const isHeader = realRowIndex < 0; - const row = isHeader ? data.columns : data.rows[realRowIndex]; - const value = row[columnIndex]; - const builder = isHeader ? this.headerBuilder : column.builder; + const isHeader = row < 0; + const rowData = isHeader ? data.columns : data.rows[row]; + const value = rowData ? rowData[column] : `[${columnIndex}:${rowIndex}]`; + const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column); return ( - {builder({ value, row, table: this, props })} + {builder({ + value, + row: rowData, + column: data.columns[column], + table: this, + props, + })} ); }; render() { - const { data, showHeader, width, height } = this.props; + const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props; + const { data } = this.state; - const columnCount = data.columns.length; - const rowCount = data.rows.length + (showHeader ? 1 : 0); + let columnCount = data.columns.length; + let rowCount = data.rows.length + (showHeader ? 1 : 0); - const fixedColumnCount = Math.min(this.props.fixedColumnCount, columnCount); - const fixedRowCount = Math.min(this.props.fixedRowCount, rowCount); + let fixedColumnCount = Math.min(fixedColumns, columnCount); + let fixedRowCount = showHeader && fixedHeader ? 1 : 0; - // Usually called after a sort or the data changes - const scrollToRow = this.scrollToTop ? 1 : -1; + if (rotate) { + const temp = columnCount; + columnCount = rowCount; + rowCount = temp; + + fixedRowCount = 0; + fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0); + } + + // Called after sort or the data changes + const scroll = this.scrollToTop ? 1 : -1; + const scrollToRow = rotate ? -1 : scroll; + const scrollToColumn = rotate ? scroll : -1; if (this.scrollToTop) { this.scrollToTop = false; } @@ -226,9 +265,10 @@ export class Table extends Component { } scrollToRow={scrollToRow} columnCount={columnCount} + scrollToColumn={scrollToColumn} rowCount={rowCount} - overscanColumnCount={2} - overscanRowCount={2} + overscanColumnCount={8} + overscanRowCount={8} columnWidth={this.measurer.columnWidth} deferredMeasurementCache={this.measurer} cellRenderer={this.cellRenderer} diff --git a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx index 9e0a111aba1..b8cb91053d8 100644 --- a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx +++ b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx @@ -11,6 +11,7 @@ import { InterpolateFunction } from '../../types/panel'; export interface TableCellBuilderOptions { value: any; + column?: Column; row?: any[]; table?: Table; className?: string; diff --git a/packages/grafana-ui/src/components/Table/examples.ts b/packages/grafana-ui/src/components/Table/examples.ts index 9f05488d839..2d08e5cdf0c 100644 --- a/packages/grafana-ui/src/components/Table/examples.ts +++ b/packages/grafana-ui/src/components/Table/examples.ts @@ -163,5 +163,5 @@ export const migratedTestStyles: ColumnStyle[] = [ export const simpleTable = { type: 'table', columns: [{ text: 'First' }, { text: 'Second' }, { text: 'Third' }], - rows: [[10, 23, 35], [11, 22, 31], [12, 21, 34]], -} as TableData; + rows: [[701, 205, 305], [702, 206, 301], [703, 207, 304]], +}; diff --git a/public/app/plugins/panel/table2/TablePanelEditor.tsx b/public/app/plugins/panel/table2/TablePanelEditor.tsx index 60d2eff9b85..ca64cc5e9d0 100644 --- a/public/app/plugins/panel/table2/TablePanelEditor.tsx +++ b/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> { @@ -11,14 +11,43 @@ export class TablePanelEditor extends PureComponent> { this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader }); }; + onToggleFixedHeader = () => { + this.props.onOptionsChange({ ...this.props.options, fixedHeader: !this.props.options.fixedHeader }); + }; + + onToggleRotate = () => { + this.props.onOptionsChange({ ...this.props.options, rotate: !this.props.options.rotate }); + }; + + onFixedColumnsChange = ({ target }) => { + this.props.onOptionsChange({ ...this.props.options, fixedColumns: target.value }); + }; + render() { - const { showHeader } = this.props.options; + const { showHeader, fixedHeader, rotate, fixedColumns } = this.props.options; return (
Header
- + + +
+ +
+
Display
+ +
); diff --git a/public/app/plugins/panel/table2/types.ts b/public/app/plugins/panel/table2/types.ts index 2d588debe58..d58c58810ef 100644 --- a/public/app/plugins/panel/table2/types.ts +++ b/public/app/plugins/panel/table2/types.ts @@ -2,11 +2,18 @@ import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder'; export interface Options { showHeader: boolean; + fixedHeader: boolean; + fixedColumns: number; + rotate: boolean; + styles: ColumnStyle[]; } export const defaults: Options = { showHeader: true, + fixedHeader: true, + fixedColumns: 0, + rotate: false, styles: [ { type: 'date', From 61053ec09919278f0d6012c2b18b0f5f558dc448 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 00:45:15 -0700 Subject: [PATCH 28/31] better css --- packages/grafana-ui/src/components/Table/Table.tsx | 6 ++---- packages/grafana-ui/src/components/Table/_Table.scss | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 4f7dce29f0d..2734bc0e706 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -212,7 +212,7 @@ export class Table extends Component { const isHeader = row < 0; const rowData = isHeader ? data.columns : data.rows[row]; - const value = rowData ? rowData[column] : `[${columnIndex}:${rowIndex}]`; + const value = rowData ? rowData[column] : ''; const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column); return ( @@ -277,10 +277,8 @@ export class Table extends Component { height={height} fixedColumnCount={fixedColumnCount} fixedRowCount={fixedRowCount} - classNameTopLeftGrid="gf-table-fixed-row-and-column" - classNameTopRightGrid="gf-table-fixed-row" + classNameTopLeftGrid="gf-table-fixed-column" classNameBottomLeftGrid="gf-table-fixed-column" - classNameBottomRightGrid="gf-table-normal-cell" /> ); } diff --git a/packages/grafana-ui/src/components/Table/_Table.scss b/packages/grafana-ui/src/components/Table/_Table.scss index 05569f84e2e..81b63d5e3c7 100644 --- a/packages/grafana-ui/src/components/Table/_Table.scss +++ b/packages/grafana-ui/src/components/Table/_Table.scss @@ -66,6 +66,8 @@ .gf-table-cell { padding: 3px 10px; + background: $page-gradient; + text-overflow: ellipsis; white-space: nowrap; @@ -74,5 +76,5 @@ } .gf-table-fixed-column { - border-right: 1px solid #ccc; + border-right: 1px solid #CCC; } From 7054cf8bb5cac908769e6e1ae6e46a8bced97bbb Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 01:08:00 -0700 Subject: [PATCH 29/31] rotate! --- packages/grafana-ui/src/components/Table/_Table.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Table/_Table.scss b/packages/grafana-ui/src/components/Table/_Table.scss index 81b63d5e3c7..d9fb2dafe6c 100644 --- a/packages/grafana-ui/src/components/Table/_Table.scss +++ b/packages/grafana-ui/src/components/Table/_Table.scss @@ -76,5 +76,5 @@ } .gf-table-fixed-column { - border-right: 1px solid #CCC; + border-right: 1px solid #ccc; } From c84c77e664339c2721c2453164089a4da4ddb744 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 08:17:59 -0700 Subject: [PATCH 30/31] onCellClick --- packages/grafana-ui/src/components/Table/Table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 2734bc0e706..7fb1a286e96 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -165,7 +165,7 @@ export class Table extends Component { } }; - handleCellClick = (rowIndex: number, columnIndex: number) => { + onCellClick = (rowIndex: number, columnIndex: number) => { const { row, column } = this.getCellRef(rowIndex, columnIndex); if (row < 0) { this.doSort(column); @@ -190,7 +190,7 @@ export class Table extends Component { } return ( -
this.handleCellClick(rowIndex, columnIndex)}> +
this.onCellClick(rowIndex, columnIndex)}> {col.text} {sorting && }
From a8c985de6073dcef6a7e8dfe8ef81c302e6519be Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 12 Mar 2019 09:09:33 -0700 Subject: [PATCH 31/31] move sort to table processing --- .../grafana-ui/src/components/Table/Table.tsx | 2 +- .../grafana-ui/src/utils/processTableData.ts | 28 +++++++++++++++++-- .../grafana-ui/src/utils/processTimeSeries.ts | 23 +-------------- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 7fb1a286e96..a551d6d221d 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -11,7 +11,7 @@ import { } from 'react-virtualized'; import { Themeable } from '../../types/theme'; -import { sortTableData } from '../../utils/processTimeSeries'; +import { sortTableData } from '../../utils/processTableData'; import { TableData, InterpolateFunction } from '@grafana/ui'; import { diff --git a/packages/grafana-ui/src/utils/processTableData.ts b/packages/grafana-ui/src/utils/processTableData.ts index b9fd0519565..d65e42827e0 100644 --- a/packages/grafana-ui/src/utils/processTableData.ts +++ b/packages/grafana-ui/src/utils/processTableData.ts @@ -1,7 +1,10 @@ -import { TableData, Column } from '../types/index'; - +// Libraries +import isNumber from 'lodash/isNumber'; import Papa, { ParseError, ParseMeta } from 'papaparse'; +// Types +import { TableData, Column } from '../types'; + // Subset of all parse options export interface TableParseOptions { headerIsFirstLine?: boolean; // Not a papa-parse option @@ -131,3 +134,24 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta columnMap: {}, }); } + +export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData { + if (isNumber(sortIndex)) { + const copy = { + ...data, + rows: [...data.rows].sort((a, b) => { + a = a[sortIndex]; + b = b[sortIndex]; + // Sort null or undefined separately from comparable values + return +(a == null) - +(b == null) || +(a > b) || -(a < b); + }), + }; + + if (reverse) { + copy.rows.reverse(); + } + + return copy; + } + return data; +} diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 3f2fded1da6..f5e9f96efba 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -4,7 +4,7 @@ import isNumber from 'lodash/isNumber'; import { colors } from './colors'; // Types -import { TimeSeries, TableData, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; +import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; interface Options { timeSeries: TimeSeries[]; @@ -173,24 +173,3 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS return vmSeries; } - -export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData { - if (isNumber(sortIndex)) { - const copy = { - ...data, - rows: [...data.rows].sort((a, b) => { - a = a[sortIndex]; - b = b[sortIndex]; - // Sort null or undefined separately from comparable values - return +(a == null) - +(b == null) || +(a > b) || -(a < b); - }), - }; - - if (reverse) { - copy.rows.reverse(); - } - - return copy; - } - return data; -}
with
@@ -11,161 +13,6 @@ xdescribe('when rendering table', () => { const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange'); describe('given 13 columns', () => { - const table = { - type: 'table', - columns: [ - { text: 'Time' }, - { text: 'Value' }, - { text: 'Colored' }, - { text: 'Undefined' }, - { text: 'String' }, - { text: 'United', unit: 'bps' }, - { text: 'Sanitized' }, - { text: 'Link' }, - { text: 'Array' }, - { text: 'Mapping' }, - { text: 'RangeMapping' }, - { text: 'MappingColored' }, - { text: 'RangeMappingColored' }, - ], - rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]], - } as TableData; - - const styles: ColumnStyle[] = [ - { - pattern: 'Time', - type: 'date', - alias: 'Timestamp', - }, - { - pattern: '/(Val)ue/', - type: 'number', - unit: 'ms', - decimals: 3, - alias: '$1', - }, - { - pattern: 'Colored', - type: 'number', - unit: 'none', - decimals: 1, - colorMode: 'value', - thresholds: [50, 80], - colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], - }, - { - pattern: 'String', - type: 'string', - }, - { - pattern: 'String', - type: 'string', - }, - { - pattern: 'United', - type: 'number', - unit: 'ms', - decimals: 2, - }, - { - pattern: 'Sanitized', - type: 'string', - sanitize: true, - }, - { - pattern: 'Link', - type: 'string', - link: true, - linkUrl: '/dashboard?param=$__cell¶m_1=$__cell_1¶m_2=$__cell_2', - linkTooltip: '$__cell $__cell_1 $__cell_6', - linkTargetBlank: true, - }, - { - pattern: 'Array', - type: 'number', - unit: 'ms', - decimals: 3, - }, - { - pattern: 'Mapping', - type: 'string', - mappingType: 1, - valueMaps: [ - { - value: '1', - text: 'on', - }, - { - value: '0', - text: 'off', - }, - { - value: 'HELLO WORLD', - text: 'HELLO GRAFANA', - }, - { - value: 'value1, value2', - text: 'value3, value4', - }, - ], - }, - { - pattern: 'RangeMapping', - type: 'string', - mappingType: 2, - rangeMaps: [ - { - from: '1', - to: '3', - text: 'on', - }, - { - from: '3', - to: '6', - text: 'off', - }, - ], - }, - { - pattern: 'MappingColored', - type: 'string', - mappingType: 1, - valueMaps: [ - { - value: '1', - text: 'on', - }, - { - value: '0', - text: 'off', - }, - ], - colorMode: 'value', - thresholds: [1, 2], - colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], - }, - { - pattern: 'RangeMappingColored', - type: 'string', - mappingType: 2, - rangeMaps: [ - { - from: '1', - to: '3', - text: 'on', - }, - { - from: '3', - to: '6', - text: 'off', - }, - ], - colorMode: 'value', - thresholds: [2, 5], - colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'], - }, - ]; - // const sanitize = value => { // return 'sanitized'; // }; @@ -179,9 +26,11 @@ xdescribe('when rendering table', () => { } return value; }; + + const table = migratedTestTable; const renderer = new Table({ - styles, - data: table, + styles: migratedTestStyles, + data: migratedTestTable, replaceVariables, showHeader: true, width: 100, diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 63ff8e55a6a..f208b7651e8 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -450,6 +450,7 @@ export class Table extends Component { if (!data) { return
NO Data
; } + return ( Date: Sat, 9 Mar 2019 00:27:33 -0800 Subject: [PATCH 22/31] table using MultiGrid --- .../src/components/Table/Table.story.tsx | 75 ++- .../src/components/Table/Table.test.ts | 4 +- .../grafana-ui/src/components/Table/Table.tsx | 456 +++++------------- .../src/components/Table/TableXXXX.tsx | 456 ++++++++++++++++++ .../src/components/Table/_Table.scss | 34 +- .../src/utils/storybook/withFullSizeStory.tsx | 23 + 6 files changed, 646 insertions(+), 402 deletions(-) create mode 100644 packages/grafana-ui/src/components/Table/TableXXXX.tsx create mode 100644 packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index a494f01c56d..8cd97dcefa9 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -1,40 +1,10 @@ -import React, { FunctionComponent } from 'react'; +// import React from 'react'; import { storiesOf } from '@storybook/react'; import { Table } from './Table'; import { migratedTestTable, migratedTestStyles, simpleTable } from './examples'; -import { ScopedVars } from '../../types/index'; - -import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; -import { AutoSizer } from 'react-virtualized'; - -const CenteredStory: FunctionComponent<{}> = ({ children }) => { - return ( -
- - {({ width, height }) => ( -
-
- Need to pass {width}/{height} to the table? -
- {children} -
- )} -
-
- ); -}; +import { ScopedVars, TableData } from '../../types/index'; +import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory'; const replaceVariables = (value: any, scopedVars: ScopedVars | undefined) => { // if (scopedVars) { @@ -47,24 +17,49 @@ const replaceVariables = (value: any, scopedVars: ScopedVars | undefined) => { }; storiesOf('UI - Alpha/Table', module) - .addDecorator(story => {story()}) .add('basic', () => { - return renderComponentWithTheme(Table, { + return withFullSizeStory(Table, { styles: [], data: simpleTable, replaceVariables, showHeader: true, - width: 500, - height: 300, }); }) .add('Test Configuration', () => { - return renderComponentWithTheme(Table, { + return withFullSizeStory(Table, { styles: migratedTestStyles, data: migratedTestTable, replaceVariables, showHeader: true, - width: 500, - height: 300, + }); + }) + .add('Lots of cells', () => { + const data = { + columns: [], + rows: [], + type: 'table', + columnMap: {}, + } as TableData; + for (let i = 0; i < 20; i++) { + data.columns.push({ + text: 'Column ' + i, + }); + } + for (let r = 0; r < 500; r++) { + const row = []; + for (let i = 0; i < 20; i++) { + row.push(r + i); + } + data.rows.push(row); + } + console.log('DATA:', data); + + return withFullSizeStory(Table, { + styles: simpleTable, + data, + replaceVariables, + showHeader: true, + fixedColumnCount: 1, + fixedRowCount: 1, }); }); diff --git a/packages/grafana-ui/src/components/Table/Table.test.ts b/packages/grafana-ui/src/components/Table/Table.test.ts index 93cf53b2adb..e53df39750e 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.ts +++ b/packages/grafana-ui/src/components/Table/Table.test.ts @@ -3,9 +3,9 @@ import _ from 'lodash'; import { getColorDefinitionByName } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui/src/types'; import { getTheme } from '../../themes'; -import Table from './Table'; 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
with
@@ -28,7 +28,7 @@ xdescribe('when rendering table', () => { }; const table = migratedTestTable; - const renderer = new Table({ + const renderer = new TableXXXX({ styles: migratedTestStyles, data: migratedTestTable, replaceVariables, diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index f208b7651e8..cbc0b50ebc0 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -1,22 +1,19 @@ // Libraries import _ from 'lodash'; -import React, { Component, CSSProperties, ReactNode } from 'react'; +import React, { Component, ReactNode } from 'react'; import { - Table as RVTable, SortDirectionType, SortIndicator, - Column as RVColumn, - TableHeaderProps, - TableCellProps, + MultiGrid, + CellMeasurerCache, + CellMeasurer, + GridCellProps, } 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 { TableData, InterpolateFunction } from '@grafana/ui'; import { ColumnStyle } from './Table'; // APP Imports!!! @@ -27,7 +24,7 @@ export interface ColumnStyle { pattern?: string; alias?: string; - colorMode?: string; + colorMode?: 'cell' | 'value'; colors?: any[]; decimals?: number; thresholds?: any[]; @@ -61,6 +58,8 @@ interface ColumnInfo { interface Props extends Themeable { data?: TableData; showHeader: boolean; + fixedColumnCount: number; + fixedRowCount: number; styles: ColumnStyle[]; replaceVariables: InterpolateFunction; width: number; @@ -78,8 +77,12 @@ export class Table extends Component { columns: ColumnInfo[] = []; colorState: any; + _cache: CellMeasurerCache; + static defaultProps = { showHeader: true, + fixedRowCount: 1, + fixedColumnCount: 0, }; constructor(props: Props) { @@ -89,6 +92,11 @@ export class Table extends Component { data: props.data, }; + this._cache = new CellMeasurerCache({ + defaultHeight: 30, + defaultWidth: 150, + }); + this.initRenderer(); } @@ -109,314 +117,17 @@ export class Table extends Component { } } - 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 = ( - - {value} - - ); - } else { - columnHtml = {value}; - } - - let filterLink: JSX.Element | null = null; - if (column.filterable) { - cellClasses.push('table-panel-cell-filterable'); - filterLink = ( - - - - - - - - - ); - } - - if (cellClasses.length) { - cellClass = cellClasses.join(' '); - } - - style.width = '100%'; - style.height = '100%'; - columnHtml = ( -
- {columnHtml} - {filterLink} -
- ); - return columnHtml; - } + initRenderer() {} //---------------------------------------------------------------------- //---------------------------------------------------------------------- - 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) { + doSort = (columnIndex: number) => { + let sort: any = this.state.sortBy; + let dir = this.state.sortDirection; + if (sort !== columnIndex) { dir = 'DESC'; + sort = columnIndex; } else if (dir === 'DESC') { dir = 'ASC'; } else { @@ -425,58 +136,107 @@ export class Table extends Component { this.setState({ sortBy: sort, sortDirection: dir }); }; - headerRenderer = (header: TableHeaderProps): ReactNode => { - const dataKey = header.dataKey as any; // types say string, but it is number! + handelClick = (rowIndex: number, columnIndex: number) => { + const { showHeader } = this.props; + const { data } = this.state; + const realRowIndex = rowIndex - (showHeader ? 1 : 0); + if (realRowIndex < 0) { + this.doSort(columnIndex); + } else { + const row = data!.rows[realRowIndex]; + const value = row[columnIndex]; + console.log('CLICK', rowIndex, columnIndex, value); + } + }; + + headerRenderer = (columnIndex: number): ReactNode => { const { data, sortBy, sortDirection } = this.state; - const col = data!.columns[dataKey]; + const col = data!.columns[columnIndex]; + const sorting = sortBy === columnIndex; return (
- {col.text} {sortBy === dataKey && } + {col.text}{' '} + {sorting && ( + + {sortDirection} + + + )}
); }; - cellRenderer = (cell: TableCellProps) => { - const { columnIndex, rowIndex } = cell; - const row = this.state.data!.rows[rowIndex]; - const val = row[columnIndex]; - return this.renderCell(columnIndex, rowIndex, val); + cellRenderer = (props: GridCellProps): React.ReactNode => { + const { rowIndex, columnIndex, key, parent, style } = props; + const { showHeader } = this.props; + const { data } = this.state; + if (!data) { + return
?
; + } + + 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 = ( +
+ {rowIndex}/{columnIndex}: {value} +
+ ); + } + + return ( + +
this.handelClick(rowIndex, columnIndex)} + className={classNames} + style={{ + ...style, + whiteSpace: 'nowrap', + }} + > + {content} +
+
+ ); }; render() { - const { width, height, showHeader } = this.props; - const { data } = this.props; + const { data, showHeader, width, height, fixedColumnCount, fixedRowCount } = this.props; if (!data) { return
NO Data
; } return ( - - {data.columns.map((col, index) => { - return ( - - ); - })} - + height={height} + fixedColumnCount={fixedColumnCount} + fixedRowCount={fixedRowCount} + classNameTopLeftGrid="gf-table-fixed-row-and-column" + classNameTopRightGrid="gf-table-fixed-row" + classNameBottomLeftGrid="gf-table-fixed-column" + classNameBottomRightGrid="gf-table-normal-cell" + /> ); } } diff --git a/packages/grafana-ui/src/components/Table/TableXXXX.tsx b/packages/grafana-ui/src/components/Table/TableXXXX.tsx new file mode 100644 index 00000000000..4c78c3b336b --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableXXXX.tsx @@ -0,0 +1,456 @@ +// 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 { + 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 = ( + + {value} + + ); + } else { + columnHtml = {value}; + } + + let filterLink: JSX.Element | null = null; + if (column.filterable) { + cellClasses.push('table-panel-cell-filterable'); + filterLink = ( + + + + + + + + + ); + } + + if (cellClasses.length) { + cellClass = cellClasses.join(' '); + } + + style.width = '100%'; + style.height = '100%'; + columnHtml = ( +
+ {columnHtml} + {filterLink} +
+ ); + 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 ( +
+ {col.text} {sortBy === dataKey && } +
+ ); + }; + + 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
NO Data
; + } + + return ( + + {data.columns.map((col, index) => { + return ( + + ); + })} + + ); + } +} + +export default TableXXXX; diff --git a/packages/grafana-ui/src/components/Table/_Table.scss b/packages/grafana-ui/src/components/Table/_Table.scss index b0aa2d6b742..f9cb0271561 100644 --- a/packages/grafana-ui/src/components/Table/_Table.scss +++ b/packages/grafana-ui/src/components/Table/_Table.scss @@ -9,12 +9,6 @@ display: flex; flex-direction: row; align-items: left; - - background: $list-item-bg; - border-top: 2px solid $body-bg; - border-bottom: 2px solid $body-bg; - - color: $blue; } .ReactVirtualized__Table__row { display: flex; @@ -37,12 +31,6 @@ margin-right: 10px; min-width: 0px; } -.ReactVirtualized__Table__rowColumn { - text-overflow: ellipsis; - white-space: nowrap; - - border-right: 2px solid $body-bg; -} .ReactVirtualized__Table__headerColumn:first-of-type, .ReactVirtualized__Table__rowColumn:first-of-type { @@ -62,3 +50,25 @@ width: 1em; fill: currentColor; } + +.gf-table-header { + padding: 3px 10px; + + background: $list-item-bg; + border-top: 2px solid $body-bg; + border-bottom: 2px solid $body-bg; + + cursor: pointer; + + color: $blue; +} + +.gf-table-cell { + padding: 3px 10px; + + text-overflow: ellipsis; + white-space: nowrap; + + border-right: 2px solid $body-bg; + border-bottom: 2px solid $body-bg; +} diff --git a/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx b/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx new file mode 100644 index 00000000000..c6efbee462f --- /dev/null +++ b/packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { AutoSizer } from 'react-virtualized'; + +export const withFullSizeStory = (component: React.ComponentType, props: any) => ( +
+ + {({ width, height }) => ( + <> + {React.createElement(component, { + ...props, + width, + height, + })} + + )} + +
+); From fe22d14e933f19cb3a18252507d8d79541a9b3b2 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 9 Mar 2019 20:47:02 -0800 Subject: [PATCH 23/31] cell builder cleanup --- .../src/components/Table/Table.story.tsx | 26 +- .../src/components/Table/Table.test.ts | 240 --------- .../grafana-ui/src/components/Table/Table.tsx | 147 +++--- .../src/components/Table/TableCellBuilder.tsx | 292 +++++++++++ .../src/components/Table/TableXXXX.tsx | 456 ------------------ .../src/components/Table/_Table.scss | 1 + .../src/components/Table/examples.ts | 2 +- .../src/utils/storybook/withFullSizeStory.tsx | 1 + public/app/plugins/panel/table/renderer.ts | 2 +- public/app/plugins/panel/table2/types.ts | 2 +- 10 files changed, 375 insertions(+), 794 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Table/Table.test.ts create mode 100644 packages/grafana-ui/src/components/Table/TableCellBuilder.tsx delete mode 100644 packages/grafana-ui/src/components/Table/TableXXXX.tsx diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 8cd97dcefa9..1aa618d73d7 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/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', () => { diff --git a/packages/grafana-ui/src/components/Table/Table.test.ts b/packages/grafana-ui/src/components/Table/Table.test.ts deleted file mode 100644 index e53df39750e..00000000000 --- a/packages/grafana-ui/src/components/Table/Table.test.ts +++ /dev/null @@ -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
with
-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('
2014-01-01T06:06:06Z2014-01-01T06:06:06Z2018-12-01T01:00:00Z2018-12-01T01:00:00Z--1.23 kbps1.230 sasd40.055.085.0value&breaking <br /> the <br /> row&breaking <br /> the <br /> rowsanitizedvalue1, value2onoffHELLO GRAFANAvalue3, value4onoffvalue1, value2ononoffoff2.10onoff7.1