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
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);