From 372e892fab1119f5a39875827c078ef627ff01bc Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 5 Mar 2019 18:17:44 -0800 Subject: [PATCH] use react-table --- .../app/plugins/panel/table2/TablePanel.tsx | 393 ++++++++++++++++-- .../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, 416 insertions(+), 128 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 => { + isUTC: false; // TODO? get UTC from props? + + columns: Column[]; + colorState: any; + + initColumns() { + this.colorState = {}; + + const { panelData, options } = this.props; + if (!panelData.tableData) { + this.columns = []; + return; + } + const { styles } = options; + + this.columns = panelData.tableData.columns.map((col, index) => { + let title = col.text; + let style: Style = null; + + for (let i = 0; i < styles.length; i++) { + const s = styles[i]; + const regex = kbn.stringToJsRegex(s.pattern); + if (title.match(regex)) { + style = s; + if (s.alias) { + title = title.replace(regex, s.alias); + } + break; + } + } + + return { + header: title, + accessor: col.text, // unique? + style: style, + formatter: this.createColumnFormatter(style, col), + }; + }); + } + + getColorForValue(value: any, style: Style) { + if (!style.thresholds) { + return null; + } + for (let i = style.thresholds.length; i > 0; i--) { + if (value >= style.thresholds[i - 1]) { + return style.colors[i]; + } + } + return _.first(style.colors); + } + + defaultCellFormatter(v: any, style: Style): string { + if (v === null || v === void 0 || v === undefined) { + return ''; + } + + if (_.isArray(v)) { + v = v.join(', '); + } + + if (style && style.sanitize) { + return sanitize(v); + } else { + return _.escape(v); + } + } + + createColumnFormatter(style: Style, header: any): CellFormatter { + if (!style) { + return this.defaultCellFormatter; + } + + if (style.type === 'hidden') { + return v => { + return undefined; + }; + } + + if (style.type === 'date') { + return v => { + if (v === undefined || v === null) { + return '-'; + } + + if (_.isArray(v)) { + v = v[0]; + } + let date = moment(v); + if (this.isUTC) { + date = date.utc(); + } + return date.format(style.dateFormat); + }; + } + + if (style.type === 'string') { + return v => { + if (_.isArray(v)) { + v = v.join(', '); + } + + const mappingType = style.mappingType || 0; + + if (mappingType === 1 && style.valueMaps) { + for (let i = 0; i < style.valueMaps.length; i++) { + const map = style.valueMaps[i]; + + if (v === null) { + if (map.value === 'null') { + return map.text; + } + continue; + } + + // Allow both numeric and string values to be mapped + if ((!_.isString(v) && Number(map.value) === Number(v)) || map.value === v) { + this.setColorState(v, style); + return this.defaultCellFormatter(map.text, style); + } + } + } + + if (mappingType === 2 && style.rangeMaps) { + for (let i = 0; i < style.rangeMaps.length; i++) { + const map = style.rangeMaps[i]; + + if (v === null) { + if (map.from === 'null' && map.to === 'null') { + return map.text; + } + continue; + } + + if (Number(map.from) <= Number(v) && Number(map.to) >= Number(v)) { + this.setColorState(v, style); + return this.defaultCellFormatter(map.text, style); + } + } + } + + if (v === null || v === void 0) { + return '-'; + } + + this.setColorState(v, style); + return this.defaultCellFormatter(v, style); + }; + } + + if (style.type === 'number') { + const valueFormatter = kbn.valueFormats[style.unit || header.unit]; + + return v => { + if (v === null || v === void 0) { + return '-'; + } + + if (_.isString(v) || _.isArray(v)) { + return this.defaultCellFormatter(v, style); + } + + this.setColorState(v, style); + return valueFormatter(v, style.decimals, null); + }; + } + + return value => { + return this.defaultCellFormatter(value, style); + }; + } + + setColorState(value: any, style: Style) { + if (!style.colorMode) { + return; + } + + if (value === null || value === void 0 || _.isArray(value)) { + return; + } + + if (_.isNaN(value)) { + return; + } + const numericValue = Number(value); + this.colorState[style.colorMode] = this.getColorForValue(numericValue, style); + } + + renderRowconstiables(rowIndex) { const { panelData } = this.props; - if (panelData.tableData) { - return panelData.tableData.rows[index.index]; + + const scopedVars = {}; + const row = panelData.tableData.rows[rowIndex]; + for (let i = 0; i < row.length; i++) { + scopedVars[`__cell_${i}`] = { value: row[i] }; } - return null; - }; + return scopedVars; + } - render() { - const { panelData, width, height, options } = this.props; - const { showHeader } = options; + renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) { + const column = this.columns[columnIndex]; + if (column.formatter) { + value = column.formatter(value, column.style); + } - const headerClassName = null; - const headerHeight = 30; - const rowHeight = 20; + const style = {}; + const cellClasses = []; + let cellClass = ''; + + if (this.colorState.cell) { + style['backgroundColor'] = this.colorState.cell; + style['color'] = 'white'; + this.colorState.cell = null; + } else if (this.colorState.value) { + style['color'] = this.colorState.value; + this.colorState.value = null; + } + + if (value === undefined) { + style['display'] = 'none'; + column.hidden = true; + } else { + column.hidden = false; + } - let rowCount = 0; + if (column.style && column.style.preserveFormat) { + cellClasses.push('table-panel-cell-pre'); + } + + let columnHtml; + if (column.style && column.style.link) { + // Render cell as link + const scopedconsts = this.renderRowconstiables(rowIndex); + scopedconsts['__cell'] = { value: value }; + + const cellLink = templateSrv.replace(column.style.linkUrl, scopedconsts, encodeURIComponent); + const cellLinkTooltip = templateSrv.replace(column.style.linkTooltip, scopedconsts); + const cellTarget = column.style.linkTargetBlank ? '_blank' : ''; + + cellClasses.push('table-panel-cell-link'); + columnHtml = ( + + {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, height, options } = this.props; + const { pageSize } = options; + + let rows = []; + let columns = []; if (panelData.tableData) { - rowCount = panelData.tableData.rows.length; + this.initColumns(); + const fields = this.columns.map(c => { + return c.accessor; + }); + rows = panelData.tableData.rows.map(row => { + return _.zipObject(fields, row); + }); + columns = this.columns.map((c, columnIndex) => { + return { + Header: c.header, + accessor: c.accessor, + filterable: !!c.filterable, + Cell: row => { + return this.renderCell(columnIndex, row.index, row.value); + }, + }; + }); + console.log(templateSrv); + console.log(rows); } else { return
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 { -}