@gooddata/react-components
Version:
GoodData.UI - A powerful JavaScript library for building analytical applications
315 lines (286 loc) • 11.1 kB
text/typescript
// (C) 2007-2020 GoodData Corporation
import { AFM, Execution } from "@gooddata/typings";
import { IDatasource, IGetRowsParams, GridApi } from "ag-grid-community";
import { IntlShape } from "react-intl";
import { getMappingHeaderName } from "../../../helpers/mappingHeader";
import { getTreeLeaves, getSubtotalStyles } from "./agGridUtils";
import { COLUMN_GROUPING_DELIMITER, ROW_ATTRIBUTE_COLUMN } from "./agGridConst";
import {
getMeasureSortItemFieldAndDirection,
getSortsFromModel,
getAttributeSortItemFieldAndDirection,
assignSorting,
} from "./agGridSorting";
import { IAgGridPage, IGridAdapterOptions, IGridHeader } from "./agGridTypes";
import { IGetPage } from "../base/VisualizationLoadingHOC";
import { IGroupingProvider } from "../pivotTable/GroupingProvider";
import {
assortDimensionHeaders,
getColumnHeaders,
getFields,
getMinimalRowData,
getRowHeaders,
} from "./agGridHeaders";
import { getRow, getRowTotals } from "./agGridData";
import { areTotalsChanged, isInvalidGetRowsRequest, wrapGetPageWithCaching } from "./agGridDataSourceUtils";
export const getDataSourceRowsGetter = (
resultSpec: AFM.IResultSpec,
getPage: IGetPage,
getExecution: () => Execution.IExecutionResponses,
onSuccess: (
execution: Execution.IExecutionResponses,
columnDefs: IGridHeader[],
resultSpec: AFM.IResultSpec,
) => void,
getGridApi: () => GridApi,
intl: IntlShape,
columnTotals: AFM.ITotalItem[],
getGroupingProvider: () => IGroupingProvider,
): ((params: IGetRowsParams) => void) => {
return (getRowsParams: IGetRowsParams) => {
const { startRow, endRow, successCallback, failCallback, sortModel } = getRowsParams;
if (isInvalidGetRowsRequest(startRow, getGridApi())) {
failCallback();
return Promise.resolve(null);
}
const execution = getExecution();
const groupingProvider = getGroupingProvider();
let resultSpecUpdated: AFM.IResultSpec = resultSpec;
// If execution is null, this means this is a fresh dataSource and we should ignore current sortModel
if (sortModel.length > 0 && execution) {
resultSpecUpdated = {
...resultSpecUpdated,
sorts: getSortsFromModel(sortModel, execution, resultSpec.sorts || []),
};
}
if (columnTotals && columnTotals.length > 0) {
resultSpecUpdated = {
...resultSpecUpdated,
dimensions: [
{
...resultSpecUpdated.dimensions[0],
totals: columnTotals,
},
...resultSpecUpdated.dimensions.slice(1),
],
};
}
const pagePromise = getPage(
resultSpecUpdated,
// column limit defaults to SERVERSIDE_COLUMN_LIMIT (1000), because 1000 columns is hopefully enough.
[endRow - startRow, undefined],
// column offset defaults to 0, because we do not support horizontal paging yet
[startRow, undefined],
);
return pagePromise.then((execution: Execution.IExecutionResponses | null) => {
if (!execution) {
return null;
}
const { columnDefs, rowData, rowTotals } = executionToAGGridAdapter(
execution,
resultSpecUpdated,
intl,
{
addLoadingRenderer: "loadingRenderer",
},
);
const { offset, count, total } = execution.executionResult.paging;
const rowAttributeIds = columnDefs
.filter(columnDef => columnDef.type === ROW_ATTRIBUTE_COLUMN)
.map(columnDef => columnDef.field);
groupingProvider.processPage(rowData, offset[0], rowAttributeIds);
// RAIL-1130: Backend returns incorrectly total: [1, N], when count: [0, N] and offset: [0, N]
const lastRow = offset[0] === 0 && count[0] === 0 ? 0 : total[0];
onSuccess(execution, columnDefs, resultSpecUpdated);
successCallback(rowData, lastRow);
// set totals
if (areTotalsChanged(getGridApi(), rowTotals)) {
getGridApi().setPinnedBottomRowData(rowTotals);
}
return execution;
});
};
};
export const executionToAGGridAdapter = (
executionResponses: Execution.IExecutionResponses,
resultSpec: AFM.IResultSpec = {},
intl: IntlShape,
options: IGridAdapterOptions = {},
): IAgGridPage => {
const { makeRowGroups = false, addLoadingRenderer = null, columnDefOptions } = options;
const {
executionResponse: { dimensions },
executionResult: { data, headerItems, totals },
} = executionResponses;
const columnAttributeHeaderCount = dimensions[1].headers.filter(
(header: Execution.IHeader) => !!(header as Execution.IAttributeHeader).attributeHeader,
).length;
const columnHeaders: IGridHeader[] = getColumnHeaders(
headerItems[1],
dimensions[1].headers,
columnDefOptions,
);
const groupColumnHeaders: IGridHeader[] =
columnAttributeHeaderCount > 0
? [
{
headerName: dimensions[1].headers
.filter(header => Execution.isAttributeHeader(header))
.map((header: Execution.IAttributeHeader) => {
return getMappingHeaderName(header);
})
.filter((item: string) => item !== null)
.join(COLUMN_GROUPING_DELIMITER),
field: "columnGroupLabel",
children: columnHeaders,
drillItems: [],
},
]
: columnHeaders;
const rowHeaders: IGridHeader[] =
// There are supposed to be only attribute headers on the first dimension
getRowHeaders(dimensions[0].headers as Execution.IAttributeHeader[], columnDefOptions, makeRowGroups);
// build sortingMap from resultSpec.sorts
const sorting = resultSpec.sorts || [];
const sortingMap = {};
const { attributeHeaders, measureHeaderItems } = assortDimensionHeaders(dimensions);
sorting.forEach(sortItem => {
if (AFM.isAttributeSortItem(sortItem)) {
const [field, direction] = getAttributeSortItemFieldAndDirection(sortItem, attributeHeaders);
sortingMap[field] = direction;
}
if (AFM.isMeasureSortItem(sortItem)) {
const [field, direction] = getMeasureSortItemFieldAndDirection(sortItem, measureHeaderItems);
sortingMap[field] = direction;
}
});
// assign sorting and indexes
const columnDefs: IGridHeader[] = [...rowHeaders, ...groupColumnHeaders].map((column, index) => {
if (column.children) {
getTreeLeaves(column).forEach((leafColumn, leafColumnIndex) => {
leafColumn.index = index + leafColumnIndex;
assignSorting(leafColumn, sortingMap);
});
}
column.index = index;
assignSorting(column, sortingMap);
return column;
});
// Add loading indicator to the first column
if (addLoadingRenderer) {
const leafColumnDefs = getTreeLeaves(columnDefs);
if (leafColumnDefs[0]) {
leafColumnDefs[0].cellRenderer = addLoadingRenderer;
}
}
const columnFields: string[] = getFields(headerItems[1]);
const rowFields: string[] = rowHeaders.map(header => header.field);
// PivotTable execution should always return a two-dimensional array (Execution.DataValue[][])
const minimalRowData: Execution.DataValue[][] = getMinimalRowData(
data as Execution.DataValue[][],
headerItems[0],
);
const subtotalStyles = getSubtotalStyles(resultSpec.dimensions ? resultSpec.dimensions[0] : null);
const rowData = minimalRowData.map((dataRow: Execution.DataValue[], dataRowIndex: number) =>
getRow(dataRow, dataRowIndex, columnFields, rowHeaders, headerItems[0], subtotalStyles, intl),
);
const columnKeys = [...rowFields, ...columnFields];
const rowTotals = getRowTotals(
totals,
columnKeys,
dimensions[0].headers,
resultSpec,
measureHeaderItems.map(mhi => mhi.measureHeaderItem.localIdentifier),
intl,
);
return {
columnDefs,
rowData,
rowTotals,
};
};
class GdToAgGridAdapter implements IDatasource {
// not needed; see IDatasource
public rowCount?: number;
private destroyed: boolean = false;
private onDestroy: () => void;
private getRowsImpl: (params: IGetRowsParams) => void;
public constructor(
resultSpec: AFM.IResultSpec,
getPage: IGetPage,
getExecution: () => Execution.IExecutionResponses,
onSuccess: (
execution: Execution.IExecutionResponses,
columnDefs: IGridHeader[],
resultSpec: AFM.IResultSpec,
) => void,
getGridApi: () => any,
intl: IntlShape,
columnTotals: AFM.ITotalItem[],
getGroupingProvider: () => IGroupingProvider,
cancelPagePromises: () => void,
) {
this.onDestroy = cancelPagePromises;
this.getRowsImpl = getDataSourceRowsGetter(
resultSpec,
wrapGetPageWithCaching(getPage),
getExecution,
onSuccess,
getGridApi,
intl,
columnTotals,
getGroupingProvider,
);
}
public getRows(params: IGetRowsParams): void {
if (this.destroyed) {
return;
}
// NOTE: some of our tests rely on getRows() to return the actual promise
return this.getRowsImpl(params);
}
public destroy(): void {
this.destroyed = true;
this.onDestroy();
}
}
/**
* Factory function to create ag-grid data source backed by GoodData executeAFM.
*
* @param resultSpec
* @param getPage
* @param getExecution
* @param onSuccess
* @param getGridApi
* @param intl
* @param columnTotals
* @param getGroupingProvider
* @param cancelPagePromises
*/
export const createAgGridDataSource = (
resultSpec: AFM.IResultSpec,
getPage: IGetPage,
getExecution: () => Execution.IExecutionResponses,
onSuccess: (
execution: Execution.IExecutionResponses,
columnDefs: IGridHeader[],
resultSpec: AFM.IResultSpec,
) => void,
getGridApi: () => any,
intl: IntlShape,
columnTotals: AFM.ITotalItem[],
getGroupingProvider: () => IGroupingProvider,
cancelPagePromises: () => void,
): IDatasource => {
return new GdToAgGridAdapter(
resultSpec,
getPage,
getExecution,
onSuccess,
getGridApi,
intl,
columnTotals,
getGroupingProvider,
cancelPagePromises,
);
};