import * as React from 'react'; import { getTransformsDataPipeline } from 'mmviz'; import { COMPONENT_MODE_INITIAL, COMPONENT_MODE_EMPTY, COMPONENT_MODE_ERROR, COMPONENT_MODE_LOADING, COMPONENT_MODE_OK, ComponentModeWindow, MmuiProps, MmuiState, MmuiTableComponent, } from '../common'; import { QueryData } from '../../mmui-data'; import { formatValue } from '../../mmui-util'; import { SummaryColumnComponent } from './refiner/SummaryColumnComponent'; import { ColumnHeadComponent } from './refiner/ColumnHeadComponent'; const ROW_MODE_CLOSED = 'closed'; const ROW_MODE_OPEN = 'open'; const ROW_MODE_LOADING = 'loading'; export interface MmuiTableProps extends MmuiProps { hasColumnAction?: boolean; hasColumnSelection?: boolean; hasLevelColumns?: boolean; //whether levels are expanded to dedicated columns transforms?: any; colOrderIgnoreCase?: boolean; //whether column sort ignores case colOrderEmptyLocation?: string; //sort location of empty values (bottom, always_top, always_bottom) reportIssueUrl?: string; // url link to report issue when something goes wrong hasPipeline?: boolean; transformsDelay?: number; tableClassNameList?: []; tableContainerClassNameList?: []; displayDefaultLabel?: boolean; } export interface MmuiTableState extends MmuiState { // contains transforms objects that are supported by mmviz.getTransformsDataPipeline function transforms?: any; rows?: any; hasPipeline?: boolean; } /** * Crafted Table Component for rendering different types of tables (regular, nested, pivoted) * The data payload is passed in the props. Either this.props.payload or this.props.store.data (store) * Create a base class that inherits from MmuiCraftedTableComponent * to override properties like this.name or this.tableClassNameList * as well as override methods such as this.getColumnSummaryRender and this.getColumnSummaryRender */ export class MmuiCraftedTableComponent< P extends MmuiTableProps, S extends MmuiTableState > extends MmuiTableComponent { // css classes to apply to the rendered table tableClassNameList; tableContainerClassNameList; numberOfItems; defaultColumnConfig: any; constructor(props: MmuiTableProps) { super(props); // css classes to apply to the rendered table this.tableClassNameList = [ 'table', 'table-sm', 'table-hover', 'mmui-table', 'mmui-table-sticky', 'mmui-table-sortable', ]; this.tableContainerClassNameList = [ 'table-responsive', 'table-responsive-short', 'mmui-table-sticky-wrapper', 'mmui-hmi-30', ]; if (props.tableClassNameList) { for (const tableClassName of props.tableClassNameList) { this.tableClassNameList.push(tableClassName); } } if (props.tableContainerClassNameList) { this.tableContainerClassNameList = props.tableContainerClassNameList; } this.defaultColumnConfig = { orderEmptyLocation: props.colOrderEmptyLocation, orderIgnoreCase: props.colOrderIgnoreCase === false ? false : true, }; } /** * @return this component's initial state. */ getInitialComponentState(): any { const state: any = { // contains transforms objects that are supported by mmviz.getTransformsDataPipeline function transforms: {}, rows: {}, isVisible: true, hasPipeline: true, }; if (this.props.payload) { state.payload = this.props.payload; } if (this.props.transforms) { state.transforms = this.props.transforms; } if (this.props.hasPipeline !== undefined) { state.hasPipeline = this.props.hasPipeline; } return state; } /** * Get the dataModel from the payload. * The dataModel should match the structure returned by the following mmvizutil functions: * mmvizutil.data.shape.shape_data * mmvizutil.data.shape.shape_data_model * mmvizutil.data.shape.shape_data_model_levels * mmvizutil.data.shape.shape_data_model_pivot */ getDataModel() { let dataModel, payload = this.getPayloadData(); if (payload) { dataModel = payload.data; } return dataModel; } /** * Extract the dataArray portion of the payload received from the server. * @param payload */ extractDataArray(payload) { let dataModel, dataArray = []; if (payload) { dataModel = payload.data; if (dataModel && dataModel.rows) { dataArray = dataModel.rows; } } return dataArray; } /** * Get the query data object that details how the data was queried */ // eslint-disable-next-line @typescript-eslint/no-unused-vars getQueryDataObj(fromStore = false) { let state = this.getComponentState(), payload = this.getPayloadData(), queryDataObj = { dim: [], dim_level: [], dim_pivot: [], }; if (state.queryDataObj) { queryDataObj = state.queryDataObj; } else if (payload && payload.data && payload.data.query) { queryDataObj = payload.data.query; } return queryDataObj; } /** * Get a copy of the rows states in the Component state. */ getStateRowsCopy(): any { const newRows: any = {}, state = this.getComponentState(); for (const rowId in state.rows) { newRows[rowId] = Object.assign({}, state.rows[rowId]); } return newRows; } /** * Create a default row state */ createStateRowDefault(): any { return { mode: ROW_MODE_CLOSED, isSelected: false, isVisibleAction: false, }; } /** * Get configuration details for the provided column * such as name, data type, if sortable, if metric, if distinct. * Override this method in a child class to customize configuration of each column (isSortable, isMetric, hasFilter) * @param column * @param config */ getColumnConfig(field, config: any = {}) { let columnConfig, defaultConfig = { name: field.toUpperCase(), columnName: field, dataType: 'categorical', isSortable: true, isDistinct: false, isNumeric: false, hasFilter: true, hasLevels: false, }, dataModel = this.getDataModel(), columnData; // check if column is in data or if column is datum attribute prefix if (Object.prototype.hasOwnProperty.call(dataModel.columns, field)) { columnData = dataModel.columns[field]; } else { const columnName = field + '_name'; if ( Object.prototype.hasOwnProperty.call( dataModel.columns, columnName ) ) { defaultConfig.columnName = columnName; columnData = dataModel.columns[columnName]; } } columnConfig = Object.assign(defaultConfig, config); if (columnData) { columnConfig.dataType = columnData.dataType; columnConfig.isNumeric = columnData.dataType == 'numerical'; columnConfig.isDistinct = columnData.is_distinct; columnConfig.isSortable = !columnConfig.isDistinct; if (dataModel.columns.hasSummary) { columnConfig.hasSummary = true; } columnConfig.hasFilter = !( columnData.extent?.length == 1 && columnData.extent[0] == '' ); } switch (field) { case 'level': columnConfig.name = 'Level'; columnConfig.columnName = 'level_name'; columnConfig.hasLevels = true; break; case 'pivot': columnConfig.dataType = 'numerical'; columnConfig.isNumeric = true; columnConfig.columnName = columnConfig.rowKey; break; default: break; } return columnConfig; } /** * Handles column row action. To be overriden by inheriting table component class. * @param e * @param data */ // eslint-disable-next-line @typescript-eslint/no-unused-vars onColumnAction(e, data) { e.preventDefault(); } /** * * @param data * @param newState */ updateRowState(row_id, newState) { let rowState: any = {}, newRows = this.getStateRowsCopy(), currentRowState = newRows[row_id]; if (currentRowState) { rowState = Object.assign(currentRowState, newState); } else { rowState = Object.assign(this.createStateRowDefault(), newState); } newRows[row_id] = rowState; this.setComponentStateValue('rows', newRows); } onColumnActionOver(e, data) { this.updateRowState(data.row_id, { isVisibleAction: true }); } onColumnActionOut(e, data) { this.updateRowState(data.row_id, { isVisibleAction: false }); } getColumnActionRender(data) { let onColumnActionClick = (e) => { this.onColumnAction(e, data); }, onColumnActionMouseOver = (e) => { this.onColumnActionOver(e, data); }, onColumnActionMouseOut = (e) => { this.onColumnActionOut(e, data); }, state = this.getComponentState(), rowState = state.rows[data.row_id], classArray = [], actionRender; if (rowState && rowState.isVisibleAction) { classArray.push('visible'); } else { classArray.push('invisible'); } actionRender = ( Filter ); return ( {actionRender} ); } /** * Get the header summary render of the provided data of the provided columnName. * @param columnName * @return (
{value}
) */ getColumnSummaryRender(field, index) { let valueFormatted, dataModel = this.getDataModel(), columnData = dataModel.columns[field], isMetric = false, classNameArray = [], summaryValue, summaryValueRender =  ; if (columnData) { isMetric = columnData.dataType == 'numerical'; summaryValue = columnData.summaryValue; if (summaryValue) { if (isMetric) { valueFormatted = summaryValue.toLocaleString('en-US'); summaryValueRender = valueFormatted; } else { summaryValueRender = summaryValue; } } } const key = `th-summary-${index}`; return ( {summaryValueRender} ); } /** * Request row handler to request the children of provided rowId. * Called in onToggleLevel, override and implement in inheriting class. * @param rowId * @param onRowRequestCallback */ // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function requestRowData(rowId, onRowRequestCallback) {} /** * Called when the a nexted level's toggle button is clicked to toggle the visibility of the level's child rows. * @param e * @param data */ onToggleLevel = (e, data) => { e.preventDefault(); e.stopPropagation(); const newRowState: any = {}, state = this.getComponentState(), currentRowState = state.rows[data.row_id]; if (data.children != null || data.child_data != null) { if ( currentRowState === undefined || currentRowState.mode === ROW_MODE_CLOSED ) { newRowState.mode = ROW_MODE_OPEN; } else { newRowState.mode = ROW_MODE_CLOSED; } } else { newRowState.mode = ROW_MODE_LOADING; } this.updateRowState(data.row_id, newRowState); if (newRowState.mode == ROW_MODE_LOADING) { this.requestRowData(data.row_id, (rowId) => { let componentState = this.getComponentState(), currentRowState = componentState.rows[rowId]; if (currentRowState === undefined) { currentRowState = this.createStateRowDefault(); } currentRowState.mode = 'open'; this.updateRowState(rowId, currentRowState); }); } }; getLevelColumnRender( columnData, data, levelDisplay, currentRowState, classNameArray, config: any = {} ) { let columnRender, levelBtn, levelIcon, columnSelectionRender = null, tLevel = (e) => { this.onToggleLevel(e, data); }; const hasChildrenProperty = Object.prototype.hasOwnProperty.call( data, 'children' ), hasChildDataProperty = Object.prototype.hasOwnProperty.call( data, 'child_data' ); if (hasChildrenProperty || hasChildDataProperty) { const hasChildren = hasChildrenProperty && (data.children == null || data.children.length > 0), hasChildData = hasChildDataProperty && (data.child_data == null || data.child_data); if (hasChildren || hasChildData) { levelIcon = ; if (currentRowState) { if (currentRowState.mode === ROW_MODE_OPEN) { levelIcon = ; } else if (currentRowState.mode === ROW_MODE_LOADING) { levelIcon = ; } } levelBtn = (
{levelIcon}
); } } if (this.props.hasColumnSelection) { const isSelected = currentRowState !== undefined && currentRowState.isSelected; const isIndeterminate = currentRowState !== undefined && currentRowState.isIndeterminate; columnSelectionRender = (
el && (el.indeterminate = isIndeterminate)} onClick={this.onRowSelectionClick} onChange={this.onRowSelectionChange} />
); } const levelItemsClassNameArray = ['d-flex', 'flex-row']; if (data.children && data.children.length == 0) { levelItemsClassNameArray.push('mmui-row-empty-nest'); } const isHead = columnData && columnData.is_head; if (isHead) { columnRender = (
{levelBtn} {columnSelectionRender}
{levelDisplay}
); } else { columnRender = (
{levelBtn} {columnSelectionRender}
{levelDisplay}
); } return columnRender; } getColumnRenderValue(field, data, isMetric) { let valueRender = data[field]; if (valueRender === undefined) { valueRender = data[field + '_name']; } if (isMetric) { valueRender = formatValue(valueRender); } return valueRender; } /** * Get the render of the provided data of the provided columnName. * Override this in a child class to customize the render of each table data element * @param columnName * @param data * @param config * @return ({value}) or ({value}) */ getColumnRender(field, data, config: any = {}) { let columnRender, valueDisplay, dataModel = this.getDataModel(), columnData = dataModel.columns[field], isHead = false, isParent = false, isMetric = false, state = this.getComponentState(), currentRowState = state.rows[data.row_id], classNameArray = []; if (columnData) { isHead = columnData.is_head; isParent = columnData.is_parent; isMetric = columnData.dataType == 'numerical'; if (isMetric) { classNameArray.push('text-right'); } } switch (field) { case 'level': classNameArray.push('mmui-level'); valueDisplay = this.getColumnRenderValue(field, data, false); columnRender = this.getLevelColumnRender( columnData, data, valueDisplay, currentRowState, classNameArray, config ); break; case 'pivot': valueDisplay = this.getColumnRenderValue( config.dataKey, data, true ); columnRender = ( {valueDisplay} ); break; default: if (isParent) { classNameArray.push('mmui-level'); valueDisplay = this.getColumnRenderValue( field, data, false ); columnRender = this.getLevelColumnRender( columnData, data, valueDisplay, currentRowState, classNameArray, config ); } else if (isHead) { let columnSelectionRender; if (this.props.hasColumnSelection) { const isSelected = currentRowState !== undefined && currentRowState.isSelected; columnSelectionRender = (
); } valueDisplay = this.getColumnRenderValue( field, data, false ); columnRender = (
{columnSelectionRender}
{valueDisplay}
); } else { valueDisplay = this.getColumnRenderValue( field, data, isMetric ); columnRender = ( {valueDisplay} ); } break; } return columnRender; } /** * Save the transforms into the state * @param transforms */ setTransforms = (transforms) => { transforms.updatedAt = new Date(); this.setComponentStateValue('transforms', transforms); }; /** * Update the selection state of the Table header based on the number of selected rows. */ getRowSelectionCount() { let row, selectionCount = 0, state = this.getComponentState(); for (const rowId in state.rows) { if ( rowId !== 'head' && Object.prototype.hasOwnProperty.call(state.rows, rowId) ) { row = state.rows[rowId]; if (row.isSelected) { selectionCount++; } } } return selectionCount; } onRowSelectionClick = (e) => { e.stopPropagation(); }; /** * Handles row selection state changes. * @param e */ onRowSelectionChange = (e) => { const input = e.target as HTMLInputElement, rowId = input.value; let newRowState, state = this.getComponentState(), rowState = state.rows[rowId]; if (rowState === undefined) { newRowState = this.createStateRowDefault(); newRowState.isSelected = true; } else { newRowState = Object.assign({}, rowState); newRowState.isSelected = !newRowState.isSelected; } this.updateRowState(rowId, newRowState); const dataModel = this.getDataModel(), newRowsState = this.getStateRowsCopy(); const selectedRow = this.findSelectedRow(dataModel.rows, rowId); newRowsState[selectedRow.row_id] = newRowState; if (selectedRow.children) { this.updateSelectionEachRecursive( selectedRow.children, newRowState.isSelected, newRowsState ); } this.updateParentSelections(dataModel.rows, newRowsState); this.numberOfItems = this.countRows(dataModel.rows); this.setComponentStateValue('rows', newRowsState); }; /** * Counts the number of rows * @param rows */ countRows(rows) { let rowCount = 0; for (const row of rows) { rowCount++; if (row.children) { rowCount = rowCount + this.countRows(row.children); } } return rowCount; } /** * This function loops through the rows and updates the selection states based on selection state of children. * @param rows * @param newRowsState */ updateParentSelections(rows, newRowsState) { let rowState, childRowState; for (const r of rows) { if (r.children) { this.updateParentSelections(r.children, newRowsState); let isAllSelected = true, isSomeSelected = false; for (const c of r.children) { childRowState = newRowsState[c.row_id]; if (childRowState) { isAllSelected = isAllSelected && childRowState.isSelected; isSomeSelected = isSomeSelected || childRowState.isSelected || childRowState.isIndeterminate; } else { isAllSelected = false; break; } } rowState = newRowsState[r.row_id]; if (rowState === undefined) { rowState = this.createStateRowDefault(); } rowState.isSelected = isAllSelected; rowState.isIndeterminate = !isAllSelected && isSomeSelected ? true : false; newRowsState[r.row_id] = rowState; } } } /** * Finds a distinct row given a set of rows and a rowId * @param rows * @param rowId */ findSelectedRow(rows, rowId) { let selectedRow; for (const row of rows) { if (row.row_id == rowId) { selectedRow = row; break; } if (row.children) { selectedRow = this.findSelectedRow(row.children, rowId); if (selectedRow) { return selectedRow; } } } return selectedRow; } /** * Set the selection state of all rows. * @param isSelected */ setRowsSelection(isSelected: boolean) { const dataModel = this.getDataModel(), newRowState = this.getStateRowsCopy(); this.numberOfItems = this.countRows(dataModel.rows); this.updateSelectionEachRecursive( dataModel.rows, isSelected, newRowState ); this.setComponentStateValue('rows', newRowState); } /** * Updates the row state of every row and counts the number of selected items * @param rows * @param isSelected * @param newRows */ updateSelectionEachRecursive(rows, isSelected, newRowsState) { let rowState; if (rows == undefined || rows.length < 1) { return; } for (const row of rows) { rowState = newRowsState[row.row_id]; if (rowState === undefined) { rowState = this.createStateRowDefault(); } rowState.isSelected = isSelected; newRowsState[row.row_id] = rowState; if (row.children) { this.updateSelectionEachRecursive( row.children, isSelected, newRowsState ); } } } /** * * @param e */ onSelectionClick = (e) => { this.setRowsSelection(e.target.checked); }; /** * Get Component mode based on data payload */ getComponentMode(payload) { let mode = COMPONENT_MODE_INITIAL, dataArray, hasData = false; if (payload) { dataArray = this.extractDataArray(payload); hasData = dataArray.length > 0; if (payload.hasError) { mode = COMPONENT_MODE_ERROR; } else if (payload.isInvalidated) { mode = COMPONENT_MODE_LOADING; } else if (hasData) { mode = COMPONENT_MODE_OK; } else { mode = COMPONENT_MODE_EMPTY; } } return mode; } /** * On update to Component props or state * @param prevProps * @param prevState */ componentDidUpdate() { const state = this.getComponentState(); if (state.operation === 'selection_clear') { this.setRowsSelection(false); this.setOperation(''); } } /** * Parse out the table header renders of pivot table into tHeadRows. * @param tHeadRows - renders of the table header rows * @param columnsData - data for all the columns * @param columnName - name of the column * @param rowStart * @param transforms - transforms applied on the data */ parsePivotTableHeaders( tHeadRows, columnsArray, columnName, rowStart, transforms ) { /** * Parse pivot table header render into tHeadRows. * @param columnsData - data for all the columns * @param dataKey - key in the data that the column corresponds with * @param rowDepth - current depth, into the header, of the row */ const parseTableHeader = (cArray, dataKey, rowDepth = 0) => { let totalColSpan = 0; if (cArray && cArray.length > 0) { let tColumns = []; if (rowDepth >= tHeadRows.length) { tHeadRows.push(tColumns); } else { tColumns = tHeadRows[rowDepth]; } for (let i = 0; i < cArray.length; i++) { let colSpan = 0, cData = cArray[i], dKey = `${dataKey}_${cData.key}`; // Recursively apply function to children. if (cData.children) { colSpan = parseTableHeader( cData.children, dKey, rowDepth + 1 ); } colSpan = colSpan === 0 ? 1 : colSpan; totalColSpan = totalColSpan + colSpan; const key = `th-${rowDepth}-${tColumns.length}`, columnConfig = this.getColumnConfig('pivot', { rowKey: cData.row_key, id: cData.id, name: cData.name, columnName: dKey, colSpan: colSpan, }); if (cData.children) { tColumns.push( {cData.name} ); } else { const completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); tColumns.push( ); } } tHeadRows[rowDepth] = tColumns; } return totalColSpan; }; return parseTableHeader(columnsArray, columnName, rowStart); } /** * Row Click event handler class method. Called by this.onRowClick. Can be overridden child class. * @param evt */ // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function rowClick(evt) {} /** * Row Click event handler. * @param evt */ onRowClick = (evt) => { this.rowClick(evt); }; /** * Parse out the row renders of table with dimension levels into tBodyRows. * @param queryData - details of the data query (dimension, levels, pivots, facts, filters) * @param tBodyRows - renders of the table body rows * @param factColumns - column names that are facts * @param rData - current row data * @param lDepth - current level depth */ parseTableLevelsRows(queryData, tBodyRows, factColumns, rData) { /** * Compose dimension level row render with fact row render into tBodyRows. * @param rowData - renders of the table body rows * @param levelDepth - current level depth */ const dimensionLevel = (rowData, levelDepth = 0) => { if (levelDepth > queryData.dimension_levels.length) { return; } let tBodyColumns = [], fact, dimension, rowRender, rowId = `${rowData.row_id}`; if (this.props.hasLevelColumns) { // let dimension_level = queryData.dimension_levels[levelDepth]; rowRender = this.getColumnRender('level', rowData, { key: `td-${tBodyRows.length}-${tBodyColumns.length}`, style: { paddingLeft: '0px' }, }); for (let i = 0; i < levelDepth; i++) { tBodyColumns.push(); } } else { rowRender = this.getColumnRender('level', rowData, { key: `td-${tBodyRows.length}-${tBodyColumns.length}`, style: { paddingLeft: `${levelDepth * 30}px` }, }); } tBodyColumns.push(rowRender); // add dimension renders if (queryData.dimensions) { for (let i = 0; i < queryData.dimensions.length; i++) { dimension = queryData.dimensions[i]; rowRender = this.getColumnRender(dimension, rowData, { key: `td-${tBodyRows.length}-${tBodyColumns.length}`, }); tBodyColumns.push(rowRender); const dimId = rowData[`${dimension}_id`]; rowId = rowId + `_${dimId}`; } } if (this.props.hasLevelColumns) { for ( let i = levelDepth; i < queryData.dimension_levels.length - 1; i++ ) { tBodyColumns.push(); } } // add fact renders for (let i = 0; i < factColumns.length; i++) { fact = factColumns[i]; rowRender = this.getColumnRender(fact, rowData, { key: `td-${tBodyRows.length}-${tBodyColumns.length}`, }); tBodyColumns.push(rowRender); } if (this.props.hasColumnAction) { tBodyColumns.push(this.getColumnActionRender(rowData)); } tBodyRows.push( {tBodyColumns} ); // Recursively apply function to children. const children = rowData['children']; if (children) { const state = this.getComponentState(), rowState = state.rows[rowData.row_id]; // only render the children if the row is open if (rowState && rowState.mode == ROW_MODE_OPEN) { for (let i = 0; i < children.length; i++) { const child = children[i]; dimensionLevel(child, levelDepth + 1); } } } }; dimensionLevel(rData); } /** * Parse out the row renders of a pivot table, with dimension levels, into tBodyRows. * @param queryData - details of the data query (dimension, levels, pivots, facts, filters) * @param dataModel - details of the data (columns, rows, pivots) * @param tBodyRows - renders of the table body rows * @param factColumns - column names that are facts * @param rData - current row data * @param lDepth - current level depth */ parseTablePivotLevelsRows( queryData, dataModel, tBodyRows, factColumns, rData, lDepth = 0 ) { /** * Compose dimension level row render with pivot data row render into tBodyRows. * @param rowData - current row data * @param levelIndex - current level depth */ const dimensionLevel = (rowData, levelDepth = 0) => { if (levelDepth > queryData.dimension_levels.length) { return; } const tBodyColumns = [], style = { paddingLeft: `${levelDepth * 30}px` }, indexColumn = this.getColumnRender('level', rowData, { key: `td-${tBodyRows.length}-${tBodyColumns.length}`, style, }); for (let c = 0; c < dataModel.columns.length; c++) { const factData = dataModel.columns[c], columnName = factData.key; for (const columnData of factData.children) { this.parsePivotDataRow( columnName, columnData, tBodyColumns, rowData ); } } tBodyRows.push( {indexColumn} {tBodyColumns} ); // Recursively apply function to children. if (rowData.children) { const state = this.getComponentState(), rowState = state.rows[rowData.row_id]; // only render the children if the row is open if (rowState && rowState.mode == ROW_MODE_OPEN) { let child; for (let i = 0; i < rowData.children.length; i++) { child = rowData.children[i]; dimensionLevel(child, levelDepth + 1); } } } }; dimensionLevel(rData, lDepth); } /** * Parse out the row renders of a pivot table, for columnName, into dataColumnRenders. * @param columnName - Name of the column * @param columnData - Data associated with the column * @param dataColumnRenders - renders of the column data * @param data - data for the current row */ parsePivotDataRow(columnName, columnData, dataColumnRenders, data) { /** * Compose pivot data column render using columnData and dataKey based on columnName. * @param cData * @param dataKey */ const renderPivotDataColumn = (cData, dataKey) => { const newDataKey = `${dataKey}_${cData.key}`; // Recursively apply function to children. if (cData.children) { // apply renderPivotDataColumn to each child for (let i = 0; i < cData.children.length; i++) { const childData = cData.children[i]; renderPivotDataColumn(childData, newDataKey); } } else { // Get data columnRender based on data key lookup into data. const columnRender = this.getColumnRender('pivot', data, { dataKey: newDataKey, }); dataColumnRenders.push( {columnRender} ); } }; renderPivotDataColumn(columnData, columnName); } getChildRender(childData) { const childPayload = { data: childData, hasError: false, isInvalidated: false, updatedAt: new Date(), }; return (
); } /** * Render the Table Component */ render() { let state = this.getComponentState(), payload = this.getPayloadData(), mode = this.getComponentMode(payload), queryData, isOkMode, isEmptyMode, transforms, preSortColumn, preSortMethod, tHead, tBody, dataRows, dataPipeline, dataModel, tBodyRows, columnCount = 0; if (state === undefined || state === null || !state.isVisible) { return null; } isOkMode = mode === COMPONENT_MODE_OK; isEmptyMode = mode === COMPONENT_MODE_EMPTY; // isErrorMode = mode === COMPONENT_MODE_ERROR; transforms = state.transforms; if (isOkMode || isEmptyMode) { dataModel = this.getDataModel(); if (state.hasPipeline) { dataPipeline = getTransformsDataPipeline(transforms); dataRows = dataPipeline.transform(dataModel.rows); } else { dataRows = dataModel.rows; } queryData = new QueryData(this.getQueryDataObj()); if (transforms === undefined || transforms.order === undefined) { preSortColumn = dataModel.order_by_column; preSortMethod = dataModel.order_by_method ? dataModel.order_by_method : 'ASC'; } const rowSelectionCount = this.getRowSelectionCount(); this.numberOfItems = this.countRows(dataModel.rows); let isHeadSelected = false, isHeadIndeterminate = false; if (this.numberOfItems != 0 && rowSelectionCount != 0) { isHeadSelected = rowSelectionCount == this.numberOfItems; isHeadIndeterminate = !isHeadSelected && rowSelectionCount > 0; } /** * Data is the result of rollup of columns with a pivot. */ if (queryData.hasPivot && queryData.hasDimensionLevels) { const tHeadRows = [], tHeadColumns = [], factColumns = queryData.factColumns, key = `th-level`, columnConfig = this.getColumnConfig('level'), completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); //determine if the dimension level is pre-sorted let preSort, levelColumnHead; if (preSortColumn === 'level') { preSort = preSortMethod; } levelColumnHead = ( ); tHeadRows.push(tHeadColumns); for (let c = 0; c < dataModel.columns.length; c++) { const factData = dataModel.columns[c], columnName = factData.key, columnConfig = this.getColumnConfig(columnName); let colSpan = this.parsePivotTableHeaders( tHeadRows, factData.children, columnName, 1, transforms ); colSpan = colSpan === 0 ? 1 : colSpan; tHeadColumns.push( {columnConfig.name} ); // add up column count for possible empty row columnCount = columnCount + colSpan; } const tHeadRowRenders = tHeadRows.map( (columnRenderArray, index) => { let rowRender = null; if (index === tHeadRows.length - 1) { rowRender = levelColumnHead; } else { rowRender = ; } return ( {rowRender} {columnRenderArray} ); } ); // add up column count for possible empty row, add one for levels column columnCount = columnCount + 1; tHead = {tHeadRowRenders}; tBodyRows = []; for (let i = 0; i < dataModel.rows.length; i++) { const rowData = dataModel.rows[i]; this.parseTablePivotLevelsRows( queryData, dataModel, tBodyRows, factColumns, rowData ); } tBody = {tBodyRows}; } else if (queryData.hasDimensionLevels) { /** * Data is the result of rollup of columns. */ let tHeadColumns = [], tSummaryColumns = [], levelColumnHead, levelColumnSummary, columnData, columnConfig, fact, dimension, dimensionLevel, factColumns = queryData.factColumns; if (this.props.hasLevelColumns) { levelColumnHead = []; let isSelectionColumn = false; for ( let i = 0; i < queryData.dimension_levels.length; i++ ) { dimensionLevel = queryData.dimension_levels[i]; columnConfig = this.getColumnConfig(dimensionLevel); isSelectionColumn = false; if (i == 0) { columnConfig.hasLevels = true; isSelectionColumn = this.props.hasColumnSelection; } //determine if the dimension level is pre-sorted let preSort; if (preSortColumn === dimensionLevel) { preSort = preSortMethod; } if (dataModel.columns.hasSummary) { columnConfig.hasSummary = true; columnConfig.summaryValueRender = this.getColumnSummaryRender(dimensionLevel, i); } const key = `th-level-${i}`; const completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); levelColumnHead.push( ); if (dataModel.columns.hasSummary) { tSummaryColumns.push( ); } } } else { const columnName = 'level'; columnConfig = this.getColumnConfig(columnName); if (dataModel.columns.hasSummary) { columnConfig.hasSummary = true; columnConfig.summaryValueRender = this.getColumnSummaryRender(columnName, 0); } const key = `th-level`; const completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); levelColumnHead = ( ); if (dataModel.columns.hasSummary) { levelColumnSummary = ( ); } } if (queryData.dimensions) { for (let i = 0; i < queryData.dimensions.length; i++) { dimension = queryData.dimensions[i]; columnConfig = this.getColumnConfig(dimension); if (columnConfig.hasSummary) { columnConfig.summaryValueRender = this.getColumnSummaryRender(dimension, i); } const completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); const key = `th-dim-${i}`; tHeadColumns.push( ); if (columnConfig.hasSummary) { tSummaryColumns.push( ); } } } for (let i = 0; i < factColumns.length; i++) { const key = `th-fact-${i}`; fact = factColumns[i]; columnConfig = this.getColumnConfig(fact); //determine if the dimension level is pre-sorted let preSort; if (preSortColumn === fact) { preSort = preSortMethod; } if (dataModel.columns.hasSummary) { columnConfig.hasSummary = true; columnConfig.summaryValueRender = this.getColumnSummaryRender(fact, i); } const completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); columnData = dataModel.columns[fact]; tHeadColumns.push( ); if (dataModel.columns.hasSummary) { tSummaryColumns.push( ); } } if (this.props.hasColumnAction) { tHeadColumns.push( ); } // set column count for possible empty row if (this.props.hasLevelColumns) { columnCount = queryData.dimension_levels.length + factColumns.length; } else { columnCount = queryData.columns.length; } tHead = ( {levelColumnHead} {tHeadColumns} {levelColumnSummary} {tSummaryColumns} ); tBodyRows = []; for (let i = 0; i < dataRows.length; i++) { const rowData = dataRows[i]; this.parseTableLevelsRows( queryData, tBodyRows, factColumns, rowData ); } tBody = {tBodyRows}; } else if (queryData.hasPivot) { /** * Data is the result of a pivot. */ const tHeadRows = [], tHeadColumns = []; tHeadRows.push(tHeadColumns); const pivotRowCount = queryData.dimensions.length, pivotRowRender = queryData.dimensions.map( (columnName, index) => { const key = `th-${index}`, columnConfig = this.getColumnConfig(columnName), completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); //determine if columnName is pre-sorted let preSort; if (preSortColumn === columnName) { preSort = preSortMethod; } return ( ); } ); // add up column count for possible empty row columnCount = columnCount + pivotRowCount; const pivotRowRenderEmpty = ; for (let i = 0; i < dataModel.columns.length; i++) { const factData = dataModel.columns[i], columnName = factData.key, columnConfig = this.getColumnConfig(columnName); let colSpan = this.parsePivotTableHeaders( tHeadRows, factData.children, columnName, 1, transforms ); colSpan = colSpan === 0 ? 1 : colSpan; tHeadColumns.push( {columnConfig.name} ); // add up column count for possible empty row columnCount = columnCount + colSpan; } const tHeadRowRenders = tHeadRows.map( (columnRenderArray, index) => { let rowRender = null; if (index === tHeadRows.length - 1) { rowRender = pivotRowRender; } else { rowRender = pivotRowRenderEmpty; } return ( {rowRender} {columnRenderArray} ); } ); tHead = {tHeadRowRenders}; //render body const dataRowRenders = []; dataRows.map((data, index) => { let dataColumnRenders = [], pivotRowName, dataColumnRender; for (let i = 0; i < queryData.dimensions.length; i++) { pivotRowName = queryData.dimensions[i]; dataColumnRender = this.getColumnRender( pivotRowName, data, { key: `td-${index}-${i}`, } ); dataColumnRenders.push(dataColumnRender); } for (let c = 0; c < dataModel.columns.length; c++) { const factData = dataModel.columns[c], columnName = factData.key; for (const columnData of factData.children) { this.parsePivotDataRow( columnName, columnData, dataColumnRenders, data ); } } dataRowRenders.push( {dataColumnRenders} ); }); tBody = {dataRowRenders}; } else { /** * Data is the result of a group by of columns. */ const tHeadColumns = queryData.columns.map( (columnName, index) => { const key = `th-${index}`, // isDistinct = dataDistinct[columnName], isDistinct = false, columnConfig = this.getColumnConfig(columnName, { isDistinct: isDistinct, }), columnData = dataModel.columns[columnName], isSelectionColumn = this.props.hasColumnSelection && index == 0; if (dataModel.columns.hasSummary) { columnConfig.hasSummary = true; columnConfig.summaryValueRender = this.getColumnSummaryRender(columnName, index); } let preSort, completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); //determine if columnName is pre-sorted if (preSortColumn === columnName) { preSort = preSortMethod; } return ( ); } ); if (this.props.hasColumnAction) { tHeadColumns.push( ); } let tSummaryColumns = null; if (dataModel.columns.hasSummary) { tSummaryColumns = queryData.columns.map( (columnName, index) => { const key = `th-${index}`, columnConfig = this.getColumnConfig( columnName, { isDistinct: false, } ); columnConfig.hasSummary = true; columnConfig.summaryValueRender = this.getColumnSummaryRender(columnName, index); const completeColumnConfig = Object.assign( columnConfig, this.defaultColumnConfig ); return ( ); } ); } tHead = ( {tHeadColumns} {tSummaryColumns} ); // set column count for possible empty row columnCount = queryData.columns.length; tBodyRows = dataRows.map((data, index) => { const hasChildRow = Object.prototype.hasOwnProperty.call( data, 'child_data' ), tBodyColumns = queryData.columns.map( (columnName, index) => { const columnRender = this.getColumnRender( columnName, data ); return ( {columnRender} ); } ); if (this.props.hasColumnAction) { tBodyColumns.push(this.getColumnActionRender(data)); } let childRenderRow, currentRowState = state.rows[data.row_id]; if ( hasChildRow && currentRowState !== undefined && currentRowState.mode === ROW_MODE_OPEN ) { const childRender = this.getChildRender( data.child_data ); childRenderRow = ( {childRender} ); } return ( {tBodyColumns} {childRenderRow} ); }); tBody = {tBodyRows}; } // create an empty row if data has been refined to no results if (dataRows.length <= 0) { tBodyRows = this.createEmptyRow(columnCount); tBody = {tBodyRows}; } } return (
{tHead} {tBody}
); } }