@deephaven/js-plugin-ag-grid
Version:
Deephaven AG Grid plugin
339 lines • 15.3 kB
JavaScript
import { TableUtils } from '@deephaven/jsapi-utils';
import Log from '@deephaven/log';
import { assertNotNull, Pending } from '@deephaven/utils';
import { getAggregatedColumns, getRollupConfig } from '../utils/AgGridAggUtils';
import { extractViewportRow, isPivotTable, isTable, isTreeTable, } from '../utils/AgGridTableUtils';
import { AgGridFilterUtils, AgGridSortUtils, isSortModelItem } from '../utils';
import { extractSnapshotRows, getPivotResultColumns, isPivotColumnGroupContext, toGroupKeyString, } from '../utils/AgGridPivotUtils';
const log = Log.module('@deephaven/js-plugin-ag-grid/ViewportDatasource');
/**
* Class that takes the input table and provides a viewport data source for AG Grid.
* Also listens for grouping to change from a table to a tree table and vice versa.
*/
export class DeephavenViewportDatasource {
/**
* Create a Deephaven Viewport Row Model data source that can be used with AG Grid.
* @param dh Deephaven API instance to use
* @param table The table to use, either a Table or TreeTable.
*/
constructor(dh, table) {
this.dh = dh;
this.table = table;
/** Store the column keys from the last snapshot. This way we can tell when we need to update the pivot result columns. */
this.lastSnapshotColumnKeys = [];
this.handleColumnGroupOpened = this.handleColumnGroupOpened.bind(this);
this.handleColumnRowGroupChanged =
this.handleColumnRowGroupChanged.bind(this);
this.handleColumnValueChanged = this.handleColumnValueChanged.bind(this);
this.handleFilterChanged = this.handleFilterChanged.bind(this);
this.handleSortChanged = this.handleSortChanged.bind(this);
this.handleTableUpdate = this.handleTableUpdate.bind(this);
this.handleTableDisconnect = this.handleTableDisconnect.bind(this);
this.originalTable = table;
this.pending = new Pending();
}
init(params) {
log.debug('Initializing DeephavenViewportDatasource', params);
this.params = params;
this.startTableListening(this.table);
}
setViewportRange(firstRow, lastRow) {
log.debug('setViewportRange', firstRow, lastRow);
this.queueOperation(async () => {
this.currentViewport = { firstRow, lastRow };
this.applyViewport(firstRow, lastRow);
});
}
/**
* Expand or collapse a row in the tree table.
*
* @param row Row to expand or collapse
* @param isExpanded Whether to expand or collapse the row
*/
setExpanded(row, isExpanded) {
log.debug('setExpanded', row);
if (isTreeTable(this.table)) {
this.table.setExpanded(row, isExpanded);
return;
}
if (isPivotTable(this.table)) {
this.table.setRowExpanded(row, isExpanded);
return;
}
throw new Error('Cannot expand/collapse rows in a non-tree table.');
}
setColumnExpanded(column, isExpanded) {
log.debug('setColumnExpanded', column);
if (isPivotTable(this.table)) {
this.table.setColumnExpanded(column, isExpanded);
return;
}
throw new Error('Cannot expand/collapse columns in a non-pivot table.');
}
setGridApi(gridApi) {
if (this.gridApi != null) {
this.gridApi.removeEventListener('columnGroupOpened', this.handleColumnGroupOpened);
this.gridApi.removeEventListener('columnRowGroupChanged', this.handleColumnRowGroupChanged);
this.gridApi.removeEventListener('columnValueChanged', this.handleColumnValueChanged);
this.gridApi.removeEventListener('filterChanged', this.handleFilterChanged);
this.gridApi.removeEventListener('sortChanged', this.handleSortChanged);
}
this.pending.cancel();
this.gridApi = gridApi;
gridApi.addEventListener('columnGroupOpened', this.handleColumnGroupOpened);
gridApi.addEventListener('columnRowGroupChanged', this.handleColumnRowGroupChanged);
gridApi.addEventListener('columnValueChanged', this.handleColumnValueChanged);
gridApi.addEventListener('filterChanged', this.handleFilterChanged);
gridApi.addEventListener('sortChanged', this.handleSortChanged);
this.queueOperation(async () => {
await this.updateGridState();
});
}
startTableListening(table) {
table.addEventListener(this.dh.Table.EVENT_UPDATED, this.handleTableUpdate);
table.addEventListener(this.dh.Table.EVENT_DISCONNECT, this.handleTableDisconnect);
}
stopTableListening(table) {
table.removeEventListener(this.dh.Table.EVENT_UPDATED, this.handleTableUpdate);
table.removeEventListener(this.dh.Table.EVENT_DISCONNECT, this.handleTableDisconnect);
}
handleColumnRowGroupChanged(event) {
log.debug('Column row group changed', event);
this.queueOperation(async () => {
await this.updateAggregations();
this.refreshViewport();
});
}
handleColumnValueChanged(event) {
log.debug('Column value changed', event);
this.queueOperation(async () => {
await this.updateAggregations();
this.refreshViewport();
});
}
handleFilterChanged(event) {
log.debug('Filter changed', event);
this.queueOperation(async () => {
this.applyFilter(this.gridApi.getFilterModel());
this.refreshViewport();
});
}
handleSortChanged(event) {
log.debug('Sort changed', event);
assertNotNull(this.gridApi);
this.queueOperation(async () => {
const columnState = this.gridApi.getColumnState();
const sortModel = columnState.filter(isSortModelItem);
this.applySort(sortModel);
this.refreshViewport();
});
}
handleColumnGroupOpened(event) {
var _a, _b, _c, _d;
log.debug('Column group opened', event);
if (!isPivotTable(this.table)) {
throw new Error('Cannot expand/collapse columns in a non-pivot table.');
}
const context = (_b = (_a = event.columnGroup) === null || _a === void 0 ? void 0 : _a.getColGroupDef()) === null || _b === void 0 ? void 0 : _b.context;
if (isPivotColumnGroupContext(context)) {
const isExpanded = (_d = (_c = event.columnGroup) === null || _c === void 0 ? void 0 : _c.isExpanded()) !== null && _d !== void 0 ? _d : true;
this.table.setColumnExpanded(context.snapshotIndex, isExpanded);
}
}
handleTableUpdate(event) {
if (isPivotTable(this.table)) {
this.handlePivotUpdate(event);
}
else {
this.handleStandardTableUpdate(event);
}
}
handleStandardTableUpdate(event) {
var _a, _b;
if (isPivotTable(this.table)) {
throw new Error('Cannot handle standard table update for pivot table.');
}
const newData = {};
const { detail: data } = event;
const { columns, offset } = data;
for (let r = 0; r < data.rows.length; r += 1) {
const row = data.rows[r];
newData[offset + r] = extractViewportRow(row, columns);
}
(_a = this.params) === null || _a === void 0 ? void 0 : _a.setRowData(newData);
(_b = this.params) === null || _b === void 0 ? void 0 : _b.setRowCount(this.table.size);
}
handlePivotUpdate(event) {
var _a, _b;
if (!isPivotTable(this.table)) {
throw new Error('Cannot handle pivot update for non-pivot table.');
}
log.debug2('Pivot update', event);
const { detail: snapshot } = event;
const rowData = extractSnapshotRows(snapshot, this.table);
log.debug2('Pivot row data', rowData);
(_a = this.params) === null || _a === void 0 ? void 0 : _a.setRowData(rowData);
(_b = this.params) === null || _b === void 0 ? void 0 : _b.setRowCount(snapshot.rows.totalCount + 1); // +1 for totals row
this.updatePivotColumnsIfNecessary(snapshot);
}
updatePivotColumnsIfNecessary(snapshot) {
var _a, _b, _c;
if (!isPivotTable(this.table)) {
throw new Error('Cannot update pivot columns for non-pivot table.');
}
const snapshotColumnKeys = [];
for (let c = 0; c < snapshot.columns.count; c += 1) {
snapshotColumnKeys.push(toGroupKeyString(snapshot.columns.getKeys(c)));
}
if (this.lastSnapshotColumnKeys.length === snapshotColumnKeys.length &&
this.lastSnapshotColumnKeys.every((value, index) => value === snapshotColumnKeys[index])) {
// No change in columns, no need to update
return;
}
this.lastSnapshotColumnKeys = snapshotColumnKeys;
const pivotResultColumns = getPivotResultColumns(snapshot.columns, this.table.valueSources);
log.debug2('Updating pivot columns', pivotResultColumns);
// We track the old pivot result column IDs so we can auto-size any new ones that were added
const oldIds = new Set((_b = (_a = this.gridApi.getPivotResultColumns()) === null || _a === void 0 ? void 0 : _a.map(c => c.getColId())) !== null && _b !== void 0 ? _b : []);
this.gridApi.setPivotResultColumns(pivotResultColumns);
const newIds = [];
const pivotColumns = (_c = this.gridApi.getPivotResultColumns()) !== null && _c !== void 0 ? _c : [];
for (let i = 0; i < pivotColumns.length; i += 1) {
const col = pivotColumns[i];
if (!oldIds.has(col.getColId())) {
newIds.push(col.getColId());
}
}
if (newIds.length > 1) {
this.gridApi.autoSizeColumns(newIds);
}
}
// eslint-disable-next-line class-methods-use-this
handleTableDisconnect() {
log.info('Table disconnected.');
}
async queueOperation(operation) {
const currentOperations = [...this.pending.pending];
return this.pending.add(Promise.all(currentOperations).then(operation));
}
applySort(sortModel) {
if (isPivotTable(this.table)) {
throw new Error('Pivot table sort not yet implemented.');
}
log.debug('Applying sort model', sortModel);
this.table.applySort(AgGridSortUtils.parseSortModel(this.table, sortModel));
}
applyFilter(filterModel) {
if (isPivotTable(this.table)) {
throw new Error('Pivot table filter not yet implemented.');
}
log.debug('Applying filter', filterModel);
this.table.applyFilter(AgGridFilterUtils.parseFilterModel(this.dh, this.table, this.gridApi.getFilterModel()));
}
applyViewport(firstRow, lastRow) {
log.debug('Applying viewport', firstRow, lastRow);
if (isPivotTable(this.table)) {
const rows = this.dh.RangeSet.ofRange(firstRow, lastRow);
// TODO: DH-20288: Viewport the pivot columns properly, instead of requesting just the first 1000.
// AG Grid does not have a good way of identifying which columns are visible, so we request all of them.
// In theory we could have placeholder columns for each column that isn't visible, but then AG Grid scrolling gets screwy. We'd need to know the width of each column to do that properly, which we can't do unless there's data.
// So, just request the first 1000 columns for now.
const columns = this.dh.RangeSet.ofRange(0, 1000);
this.table.setViewport({
rows,
columns,
sources: this.table.valueSources,
});
}
else {
this.table.setViewport(firstRow, lastRow);
}
}
refreshViewport() {
log.debug('Refreshing viewport');
if (this.currentViewport == null) {
const defaultViewport = {
firstRow: Math.max(this.gridApi.getFirstDisplayedRowIndex(), 0),
lastRow: Math.max(this.gridApi.getLastDisplayedRowIndex(), 0),
};
log.debug('Setting default viewport', defaultViewport);
this.currentViewport = defaultViewport;
}
const { firstRow, lastRow } = this.currentViewport;
this.applyViewport(firstRow, lastRow);
}
/**
* Syncs this data source with the current GridApi state.
* This includes applying the current filter, sort, and viewport.
*/
async updateGridState() {
log.debug('Updating grid state');
if (isTable(this.originalTable)) {
// Start by updating the aggregations. This may produce a new table which filters may or may not apply to.
await this.updateAggregations();
}
if (!isPivotTable(this.originalTable)) {
this.updateFilter();
this.updateSort();
}
this.refreshViewport();
}
/** Syncs the filter with the GridApi */
updateFilter() {
log.debug('Updating filter');
const filterModel = this.gridApi.getFilterModel();
this.applyFilter(filterModel);
}
/** Syncs the sort with the GridApi */
updateSort() {
const columnState = this.gridApi.getColumnState();
const sortModel = columnState.filter(isSortModelItem);
this.applySort(sortModel);
}
/**
* Get the current row group columns and aggregations and apply them.
*/
async updateAggregations() {
assertNotNull(this.gridApi);
const rowGroupColumns = this.gridApi.getRowGroupColumns();
const aggregatedColumns = getAggregatedColumns(this.gridApi);
log.debug('Updating aggregations', rowGroupColumns, aggregatedColumns);
if (rowGroupColumns.length === 0) {
log.debug('No row group columns, using original table');
this.setTable(this.originalTable);
this.refreshViewport();
return;
}
const rollupConfig = getRollupConfig(rowGroupColumns, aggregatedColumns, this.dh);
if (TableUtils.isTreeTable(this.originalTable)) {
throw new Error('Cannot apply aggregations to a tree table.');
}
if (isPivotTable(this.originalTable)) {
throw new Error('Cannot apply aggregations to a pivot table.');
}
const treeTable = await this.originalTable.rollup(rollupConfig);
this.setTable(treeTable);
this.updateFilter();
this.updateSort();
this.refreshViewport();
}
setTable(table) {
log.debug('Setting table', table);
this.stopTableListening(this.table);
if (this.originalTable !== this.table && table !== this.table) {
log.debug('Closing table', this.table);
this.table.close();
}
this.table = table;
this.startTableListening(table);
}
destroy() {
log.debug('Destroying DeephavenViewportDatasource');
this.table.close();
if (this.originalTable !== this.table) {
this.originalTable.close();
}
}
}
export default DeephavenViewportDatasource;
//# sourceMappingURL=DeephavenViewportDatasource.js.map