/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Component } from 'react'; import { safeHtmlSpan } from '@superset-ui/core'; import { t } from '@apache-superset/core/ui'; import PropTypes from 'prop-types'; import { FaSort } from '@react-icons/all-files/fa/FaSort'; import { FaSortDown as FaSortDesc } from '@react-icons/all-files/fa/FaSortDown'; import { FaSortUp as FaSortAsc } from '@react-icons/all-files/fa/FaSortUp'; import { PivotData, flatKey } from './utilities'; import { Styles } from './Styles'; const parseLabel = value => { if (typeof value === 'string') { if (value === 'metric') return t('metric'); return value; } if (typeof value === 'number') { return value; } return String(value); }; function displayCell(value, allowRenderHtml) { if (allowRenderHtml && typeof value === 'string') { return safeHtmlSpan(value); } return parseLabel(value); } function displayHeaderCell( needToggle, ArrowIcon, onArrowClick, value, namesMapping, allowRenderHtml, ) { const name = namesMapping[value] || value; const parsedLabel = parseLabel(name); const labelContent = allowRenderHtml && typeof parsedLabel === 'string' ? safeHtmlSpan(parsedLabel) : parsedLabel; return needToggle ? ( {ArrowIcon} {labelContent} ) : ( labelContent ); } function sortHierarchicalObject(obj, objSort, rowPartialOnTop) { // Performs a recursive sort of nested object structures. Sorts objects based on // their currentVal property. The function preserves the hierarchical structure // while sorting each level according to the specified criteria. const sortedKeys = Object.keys(obj).sort((a, b) => { const valA = obj[a].currentVal || 0; const valB = obj[b].currentVal || 0; if (rowPartialOnTop) { if (obj[a].currentVal !== undefined && obj[b].currentVal === undefined) { return -1; } if (obj[b].currentVal !== undefined && obj[a].currentVal === undefined) { return 1; } } return objSort === 'asc' ? valA - valB : valB - valA; }); const result = new Map(); sortedKeys.forEach(key => { const value = obj[key]; if (typeof value === 'object' && !Array.isArray(value)) { result.set(key, sortHierarchicalObject(value, objSort, rowPartialOnTop)); } else { result.set(key, value); } }); return result; } function convertToArray( obj, rowEnabled, rowPartialOnTop, maxRowIndex, parentKeys = [], result = [], flag = false, ) { // Recursively flattens a hierarchical Map structure into an array of key paths. // Handles different rendering scenarios based on row grouping configurations and // depth limitations. The function supports complex hierarchy flattening with let updatedFlag = flag; const keys = Array.from(obj.keys()); const getValue = key => obj.get(key); keys.forEach(key => { if (key === 'currentVal') { return; } const value = getValue(key); if (rowEnabled && rowPartialOnTop && parentKeys.length < maxRowIndex - 1) { result.push(parentKeys.length > 0 ? [...parentKeys, key] : [key]); updatedFlag = true; } if (typeof value === 'object' && !Array.isArray(value)) { convertToArray( value, rowEnabled, rowPartialOnTop, maxRowIndex, [...parentKeys, key], result, ); } if ( parentKeys.length >= maxRowIndex - 1 || (rowEnabled && !rowPartialOnTop) ) { if (!updatedFlag) { result.push(parentKeys.length > 0 ? [...parentKeys, key] : [key]); return; } } if (parentKeys.length === 0 && maxRowIndex === 1) { result.push([key]); } }); return result; } export class TableRenderer extends Component { constructor(props) { super(props); // We need state to record which entries are collapsed and which aren't. // This is an object with flat-keys indicating if the corresponding rows // should be collapsed. this.state = { collapsedRows: {}, collapsedCols: {}, sortingOrder: [] }; this.sortCache = new Map(); this.clickHeaderHandler = this.clickHeaderHandler.bind(this); this.clickHandler = this.clickHandler.bind(this); } getBasePivotSettings() { // One-time extraction of pivot settings that we'll use throughout the render. const { props } = this; const colAttrs = props.cols; const rowAttrs = props.rows; const tableOptions = { rowTotals: true, colTotals: true, ...props.tableOptions, }; const rowTotals = tableOptions.rowTotals || colAttrs.length === 0; const colTotals = tableOptions.colTotals || rowAttrs.length === 0; const namesMapping = props.namesMapping || {}; const subtotalOptions = { arrowCollapsed: '\u25B2', arrowExpanded: '\u25BC', ...props.subtotalOptions, }; const colSubtotalDisplay = { displayOnTop: false, enabled: tableOptions.colSubTotals, hideOnExpand: false, ...subtotalOptions.colSubtotalDisplay, }; const rowSubtotalDisplay = { displayOnTop: false, enabled: tableOptions.rowSubTotals, hideOnExpand: false, ...subtotalOptions.rowSubtotalDisplay, }; const pivotData = new PivotData(props, { rowEnabled: rowSubtotalDisplay.enabled, colEnabled: colSubtotalDisplay.enabled, rowPartialOnTop: rowSubtotalDisplay.displayOnTop, colPartialOnTop: colSubtotalDisplay.displayOnTop, }); const rowKeys = pivotData.getRowKeys(); const colKeys = pivotData.getColKeys(); // Also pre-calculate all the callbacks for cells, etc... This is nice to have to // avoid re-calculations of the call-backs on cell expansions, etc... const cellCallbacks = {}; const rowTotalCallbacks = {}; const colTotalCallbacks = {}; let grandTotalCallback = null; if (tableOptions.clickCallback) { rowKeys.forEach(rowKey => { const flatRowKey = flatKey(rowKey); if (!(flatRowKey in cellCallbacks)) { cellCallbacks[flatRowKey] = {}; } colKeys.forEach(colKey => { cellCallbacks[flatRowKey][flatKey(colKey)] = this.clickHandler( pivotData, rowKey, colKey, ); }); }); // Add in totals as well. if (rowTotals) { rowKeys.forEach(rowKey => { rowTotalCallbacks[flatKey(rowKey)] = this.clickHandler( pivotData, rowKey, [], ); }); } if (colTotals) { colKeys.forEach(colKey => { colTotalCallbacks[flatKey(colKey)] = this.clickHandler( pivotData, [], colKey, ); }); } if (rowTotals && colTotals) { grandTotalCallback = this.clickHandler(pivotData, [], []); } } return { pivotData, colAttrs, rowAttrs, colKeys, rowKeys, rowTotals, colTotals, arrowCollapsed: subtotalOptions.arrowCollapsed, arrowExpanded: subtotalOptions.arrowExpanded, colSubtotalDisplay, rowSubtotalDisplay, cellCallbacks, rowTotalCallbacks, colTotalCallbacks, grandTotalCallback, namesMapping, allowRenderHtml: props.allowRenderHtml, }; } clickHandler(pivotData, rowValues, colValues) { const colAttrs = this.props.cols; const rowAttrs = this.props.rows; const value = pivotData.getAggregator(rowValues, colValues).value(); const filters = {}; const colLimit = Math.min(colAttrs.length, colValues.length); for (let i = 0; i < colLimit; i += 1) { const attr = colAttrs[i]; if (colValues[i] !== null) { filters[attr] = colValues[i]; } } const rowLimit = Math.min(rowAttrs.length, rowValues.length); for (let i = 0; i < rowLimit; i += 1) { const attr = rowAttrs[i]; if (rowValues[i] !== null) { filters[attr] = rowValues[i]; } } return e => this.props.tableOptions.clickCallback(e, value, filters, pivotData); } clickHeaderHandler( pivotData, values, attrs, attrIdx, callback, isSubtotal = false, isGrandTotal = false, ) { const filters = {}; for (let i = 0; i <= attrIdx; i += 1) { const attr = attrs[i]; filters[attr] = values[i]; } return e => callback( e, values[attrIdx], filters, pivotData, isSubtotal, isGrandTotal, ); } collapseAttr(rowOrCol, attrIdx, allKeys) { return e => { // Collapse an entire attribute. e.stopPropagation(); const keyLen = attrIdx + 1; const collapsed = allKeys.filter(k => k.length === keyLen).map(flatKey); const updates = {}; collapsed.forEach(k => { updates[k] = true; }); if (rowOrCol) { this.setState(state => ({ collapsedRows: { ...state.collapsedRows, ...updates }, })); } else { this.setState(state => ({ collapsedCols: { ...state.collapsedCols, ...updates }, })); } }; } expandAttr(rowOrCol, attrIdx, allKeys) { return e => { // Expand an entire attribute. This implicitly implies expanding all of the // parents as well. It's a bit inefficient but ah well... e.stopPropagation(); const updates = {}; allKeys.forEach(k => { for (let i = 0; i <= attrIdx; i += 1) { updates[flatKey(k.slice(0, i + 1))] = false; } }); if (rowOrCol) { this.setState(state => ({ collapsedRows: { ...state.collapsedRows, ...updates }, })); } else { this.setState(state => ({ collapsedCols: { ...state.collapsedCols, ...updates }, })); } }; } toggleRowKey(flatRowKey) { return e => { e.stopPropagation(); this.setState(state => ({ collapsedRows: { ...state.collapsedRows, [flatRowKey]: !state.collapsedRows[flatRowKey], }, })); }; } toggleColKey(flatColKey) { return e => { e.stopPropagation(); this.setState(state => ({ collapsedCols: { ...state.collapsedCols, [flatColKey]: !state.collapsedCols[flatColKey], }, })); }; } calcAttrSpans(attrArr, numAttrs) { // Given an array of attribute values (i.e. each element is another array with // the value at every level), compute the spans for every attribute value at // every level. The return value is a nested array of the same shape. It has // -1's for repeated values and the span number otherwise. const spans = []; // Index of the last new value const li = Array(numAttrs).map(() => 0); let lv = Array(numAttrs).map(() => null); for (let i = 0; i < attrArr.length; i += 1) { // Keep increasing span values as long as the last keys are the same. For // the rest, record spans of 1. Update the indices too. const cv = attrArr[i]; const ent = []; let depth = 0; const limit = Math.min(lv.length, cv.length); while (depth < limit && lv[depth] === cv[depth]) { ent.push(-1); spans[li[depth]][depth] += 1; depth += 1; } while (depth < cv.length) { li[depth] = i; ent.push(1); depth += 1; } spans.push(ent); lv = cv; } return spans; } getAggregatedData(pivotData, visibleColName, rowPartialOnTop) { // Transforms flat row keys into a hierarchical group structure where each level // represents a grouping dimension. For each row key path, it calculates the // aggregated value for the specified column and builds a nested object that // preserves the hierarchy while storing aggregation values at each level. const groups = {}; const rows = pivotData.rowKeys; rows.forEach(rowKey => { const aggValue = pivotData.getAggregator(rowKey, visibleColName).value() ?? 0; if (rowPartialOnTop) { const parent = rowKey .slice(0, -1) .reduce((acc, key) => (acc[key] ??= {}), groups); parent[rowKey.at(-1)] = { currentVal: aggValue }; } else { rowKey.reduce((acc, key) => { acc[key] = acc[key] || { currentVal: 0 }; acc[key].currentVal = aggValue; return acc[key]; }, groups); } }); return groups; } sortAndCacheData( groups, sortOrder, rowEnabled, rowPartialOnTop, maxRowIndex, ) { // Processes hierarchical data by first sorting it according to the specified order // and then converting the sorted structure into a flat array format. This function // serves as an intermediate step between hierarchical data representation and // flat array representation needed for rendering. const sortedGroups = sortHierarchicalObject( groups, sortOrder, rowPartialOnTop, ); return convertToArray( sortedGroups, rowEnabled, rowPartialOnTop, maxRowIndex, ); } sortData(columnIndex, visibleColKeys, pivotData, maxRowIndex) { // Handles column sorting with direction toggling (asc/desc) and implements // caching mechanism to avoid redundant sorting operations. When sorting the same // column multiple times, it cycles through sorting directions. Uses composite // cache keys based on sorting parameters for optimal performance. this.setState(state => { const { sortingOrder, activeSortColumn } = state; const newSortingOrder = []; let newDirection = 'asc'; if (activeSortColumn === columnIndex) { newDirection = sortingOrder[columnIndex] === 'asc' ? 'desc' : 'asc'; } const { rowEnabled, rowPartialOnTop } = pivotData.subtotals; newSortingOrder[columnIndex] = newDirection; const cacheKey = `${columnIndex}-${visibleColKeys.length}-${rowEnabled}-${rowPartialOnTop}-${newDirection}`; let newRowKeys; if (this.sortCache.has(cacheKey)) { const cachedRowKeys = this.sortCache.get(cacheKey); newRowKeys = cachedRowKeys; } else { const groups = this.getAggregatedData( pivotData, visibleColKeys[columnIndex], rowPartialOnTop, ); const sortedRowKeys = this.sortAndCacheData( groups, newDirection, rowEnabled, rowPartialOnTop, maxRowIndex, ); this.sortCache.set(cacheKey, sortedRowKeys); newRowKeys = sortedRowKeys; } this.cachedBasePivotSettings = { ...this.cachedBasePivotSettings, rowKeys: newRowKeys, }; return { sortingOrder: newSortingOrder, activeSortColumn: columnIndex, }; }); } renderColHeaderRow(attrName, attrIdx, pivotSettings) { // Render a single row in the column header at the top of the pivot table. const { rowAttrs, colAttrs, colKeys, visibleColKeys, colAttrSpans, rowTotals, arrowExpanded, arrowCollapsed, colSubtotalDisplay, maxColVisible, pivotData, namesMapping, allowRenderHtml, } = pivotSettings; const { highlightHeaderCellsOnHover, omittedHighlightHeaderGroups = [], highlightedHeaderCells, dateFormatters, } = this.props.tableOptions; const spaceCell = attrIdx === 0 && rowAttrs.length !== 0 ? ( ) : null; const needToggle = colSubtotalDisplay.enabled && attrIdx !== colAttrs.length - 1; let arrowClickHandle = null; let subArrow = null; if (needToggle) { arrowClickHandle = attrIdx + 1 < maxColVisible ? this.collapseAttr(false, attrIdx, colKeys) : this.expandAttr(false, attrIdx, colKeys); subArrow = attrIdx + 1 < maxColVisible ? arrowExpanded : arrowCollapsed; } const attrNameCell = ( {displayHeaderCell( needToggle, subArrow, arrowClickHandle, attrName, namesMapping, allowRenderHtml, )} ); const attrValueCells = []; const rowIncrSpan = rowAttrs.length !== 0 ? 1 : 0; // Iterate through columns. Jump over duplicate values. let i = 0; while (i < visibleColKeys.length) { let handleContextMenu; const colKey = visibleColKeys[i]; const colSpan = attrIdx < colKey.length ? colAttrSpans[i][attrIdx] : 1; let colLabelClass = 'pvtColLabel'; if (attrIdx < colKey.length) { if (!omittedHighlightHeaderGroups.includes(colAttrs[attrIdx])) { if (highlightHeaderCellsOnHover) { colLabelClass += ' hoverable'; } handleContextMenu = e => this.props.onContextMenu(e, colKey, undefined, { [attrName]: colKey[attrIdx], }); } if ( highlightedHeaderCells && Array.isArray(highlightedHeaderCells[colAttrs[attrIdx]]) && highlightedHeaderCells[colAttrs[attrIdx]].includes(colKey[attrIdx]) ) { colLabelClass += ' active'; } const { maxRowVisible: maxRowIndex, maxColVisible } = pivotSettings; const visibleSortIcon = maxColVisible - 1 === attrIdx; const columnName = colKey[maxColVisible - 1]; const rowSpan = 1 + (attrIdx === colAttrs.length - 1 ? rowIncrSpan : 0); const flatColKey = flatKey(colKey.slice(0, attrIdx + 1)); const onArrowClick = needToggle ? this.toggleColKey(flatColKey) : null; const getSortIcon = key => { const { activeSortColumn, sortingOrder } = this.state; if (activeSortColumn !== key) { return ( this.sortData(key, visibleColKeys, pivotData, maxRowIndex) } /> ); } const SortIcon = sortingOrder[key] === 'asc' ? FaSortAsc : FaSortDesc; return ( this.sortData(key, visibleColKeys, pivotData, maxRowIndex) } /> ); }; const headerCellFormattedValue = dateFormatters && dateFormatters[attrName] && typeof dateFormatters[attrName] === 'function' ? dateFormatters[attrName](colKey[attrIdx]) : colKey[attrIdx]; attrValueCells.push( {displayHeaderCell( needToggle, this.state.collapsedCols[flatColKey] ? arrowCollapsed : arrowExpanded, onArrowClick, headerCellFormattedValue, namesMapping, allowRenderHtml, )} { e.stopPropagation(); }} aria-label={ this.state.activeSortColumn === i ? `Sorted by ${columnName} ${this.state.sortingOrder[i] === 'asc' ? 'ascending' : 'descending'}` : undefined } > {visibleSortIcon && getSortIcon(i)} , ); } else if (attrIdx === colKey.length) { const rowSpan = colAttrs.length - colKey.length + rowIncrSpan; attrValueCells.push( {t('Subtotal')} , ); } // The next colSpan columns will have the same value anyway... i += colSpan; } const totalCell = attrIdx === 0 && rowTotals ? ( {t('Total (%(aggregatorName)s)', { aggregatorName: t(this.props.aggregatorName), })} ) : null; const cells = [spaceCell, attrNameCell, ...attrValueCells, totalCell]; return {cells}; } renderRowHeaderRow(pivotSettings) { // Render just the attribute names of the rows (the actual attribute values // will show up in the individual rows). const { rowAttrs, colAttrs, rowKeys, arrowCollapsed, arrowExpanded, rowSubtotalDisplay, maxRowVisible, pivotData, namesMapping, allowRenderHtml, } = pivotSettings; return ( {rowAttrs.map((r, i) => { const needLabelToggle = rowSubtotalDisplay.enabled && i !== rowAttrs.length - 1; let arrowClickHandle = null; let subArrow = null; if (needLabelToggle) { arrowClickHandle = i + 1 < maxRowVisible ? this.collapseAttr(true, i, rowKeys) : this.expandAttr(true, i, rowKeys); subArrow = i + 1 < maxRowVisible ? arrowExpanded : arrowCollapsed; } return ( {displayHeaderCell( needLabelToggle, subArrow, arrowClickHandle, r, namesMapping, allowRenderHtml, )} ); })} {colAttrs.length === 0 ? t('Total (%(aggregatorName)s)', { aggregatorName: t(this.props.aggregatorName), }) : null} ); } renderTableRow(rowKey, rowIdx, pivotSettings) { // Render a single row in the pivot table. const { rowAttrs, colAttrs, rowAttrSpans, visibleColKeys, pivotData, rowTotals, rowSubtotalDisplay, arrowExpanded, arrowCollapsed, cellCallbacks, rowTotalCallbacks, namesMapping, allowRenderHtml, } = pivotSettings; const { highlightHeaderCellsOnHover, omittedHighlightHeaderGroups = [], highlightedHeaderCells, cellColorFormatters, dateFormatters, } = this.props.tableOptions; const flatRowKey = flatKey(rowKey); const colIncrSpan = colAttrs.length !== 0 ? 1 : 0; const attrValueCells = rowKey.map((r, i) => { let handleContextMenu; let valueCellClassName = 'pvtRowLabel'; if (!omittedHighlightHeaderGroups.includes(rowAttrs[i])) { if (highlightHeaderCellsOnHover) { valueCellClassName += ' hoverable'; } handleContextMenu = e => this.props.onContextMenu(e, undefined, rowKey, { [rowAttrs[i]]: r, }); } if ( highlightedHeaderCells && Array.isArray(highlightedHeaderCells[rowAttrs[i]]) && highlightedHeaderCells[rowAttrs[i]].includes(r) ) { valueCellClassName += ' active'; } const rowSpan = rowAttrSpans[rowIdx][i]; if (rowSpan > 0) { const flatRowKey = flatKey(rowKey.slice(0, i + 1)); const colSpan = 1 + (i === rowAttrs.length - 1 ? colIncrSpan : 0); const needRowToggle = rowSubtotalDisplay.enabled && i !== rowAttrs.length - 1; const onArrowClick = needRowToggle ? this.toggleRowKey(flatRowKey) : null; const headerCellFormattedValue = dateFormatters && dateFormatters[rowAttrs[i]] ? dateFormatters[rowAttrs[i]](r) : r; return ( {displayHeaderCell( needRowToggle, this.state.collapsedRows[flatRowKey] ? arrowCollapsed : arrowExpanded, onArrowClick, headerCellFormattedValue, namesMapping, allowRenderHtml, )} ); } return null; }); const attrValuePaddingCell = rowKey.length < rowAttrs.length ? ( {t('Subtotal')} ) : null; const rowClickHandlers = cellCallbacks[flatRowKey] || {}; const valueCells = visibleColKeys.map(colKey => { const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator(rowKey, colKey); const aggValue = agg.value(); const keys = [...rowKey, ...colKey]; let backgroundColor; if (cellColorFormatters) { Object.values(cellColorFormatters).forEach(cellColorFormatter => { if (Array.isArray(cellColorFormatter)) { keys.forEach(key => { if (backgroundColor) { return; } cellColorFormatter .filter(formatter => formatter.column === key) .forEach(formatter => { const formatterResult = formatter.getColorFromValue(aggValue); if (formatterResult) { backgroundColor = formatterResult; } }); }); } }); } const style = agg.isSubtotal ? { fontWeight: 'bold' } : { backgroundColor }; return ( this.props.onContextMenu(e, colKey, rowKey)} style={style} > {displayCell(agg.format(aggValue, agg), allowRenderHtml)} ); }); let totalCell = null; if (rowTotals) { const agg = pivotData.getAggregator(rowKey, []); const aggValue = agg.value(); totalCell = ( this.props.onContextMenu(e, undefined, rowKey)} > {displayCell(agg.format(aggValue, agg), allowRenderHtml)} ); } const rowCells = [ ...attrValueCells, attrValuePaddingCell, ...valueCells, totalCell, ]; return {rowCells}; } renderTotalsRow(pivotSettings) { // Render the final totals rows that has the totals for all the columns. const { rowAttrs, colAttrs, visibleColKeys, rowTotals, pivotData, colTotalCallbacks, grandTotalCallback, } = pivotSettings; const totalLabelCell = ( {t('Total (%(aggregatorName)s)', { aggregatorName: t(this.props.aggregatorName), })} ); const totalValueCells = visibleColKeys.map(colKey => { const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator([], colKey); const aggValue = agg.value(); return ( this.props.onContextMenu(e, colKey, undefined)} style={{ padding: '5px' }} > {displayCell(agg.format(aggValue, agg), this.props.allowRenderHtml)} ); }); let grandTotalCell = null; if (rowTotals) { const agg = pivotData.getAggregator([], []); const aggValue = agg.value(); grandTotalCell = ( this.props.onContextMenu(e, undefined, undefined)} > {displayCell(agg.format(aggValue, agg), this.props.allowRenderHtml)} ); } const totalCells = [totalLabelCell, ...totalValueCells, grandTotalCell]; return ( {totalCells} ); } visibleKeys(keys, collapsed, numAttrs, subtotalDisplay) { return keys.filter( key => // Is the key hidden by one of its parents? !key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) && // Leaf key. (key.length === numAttrs || // Children hidden. Must show total. flatKey(key) in collapsed || // Don't hide totals. !subtotalDisplay.hideOnExpand), ); } isDashboardEditMode() { return document.contains(document.querySelector('.dashboard--editing')); } componentWillUnmount() { this.sortCache.clear(); } render() { if (this.cachedProps !== this.props) { this.sortCache.clear(); this.state.sortingOrder = []; this.state.activeSortColumn = null; this.cachedProps = this.props; this.cachedBasePivotSettings = this.getBasePivotSettings(); } const { colAttrs, rowAttrs, rowKeys, colKeys, colTotals, rowSubtotalDisplay, colSubtotalDisplay, allowRenderHtml, } = this.cachedBasePivotSettings; // Need to account for exclusions to compute the effective row // and column keys. const visibleRowKeys = this.visibleKeys( rowKeys, this.state.collapsedRows, rowAttrs.length, rowSubtotalDisplay, ); const visibleColKeys = this.visibleKeys( colKeys, this.state.collapsedCols, colAttrs.length, colSubtotalDisplay, ); const pivotSettings = { visibleRowKeys, maxRowVisible: Math.max(...visibleRowKeys.map(k => k.length)), visibleColKeys, maxColVisible: Math.max(...visibleColKeys.map(k => k.length)), rowAttrSpans: this.calcAttrSpans(visibleRowKeys, rowAttrs.length), colAttrSpans: this.calcAttrSpans(visibleColKeys, colAttrs.length), allowRenderHtml, ...this.cachedBasePivotSettings, }; return ( {colAttrs.map((c, j) => this.renderColHeaderRow(c, j, pivotSettings), )} {rowAttrs.length !== 0 && this.renderRowHeaderRow(pivotSettings)} {visibleRowKeys.map((r, i) => this.renderTableRow(r, i, pivotSettings), )} {colTotals && this.renderTotalsRow(pivotSettings)}
); } } TableRenderer.propTypes = { ...PivotData.propTypes, tableOptions: PropTypes.object, onContextMenu: PropTypes.func, }; TableRenderer.defaultProps = { ...PivotData.defaultProps, tableOptions: {} };