UNPKG

ag-grid-enterprise

Version:

ag-Grid Enterprise Features

624 lines (515 loc) 23.2 kB
import { _, Autowired, Bean, BeanStub, Column, ColumnController, ColumnVO, Constants, Context, Events, EventService, FilterManager, GridOptionsWrapper, IServerSideDatasource, IServerSideRowModel, Logger, LoggerFactory, ModelUpdatedEvent, NumberSequence, PostConstruct, Qualifier, RowNode, RowNodeBlockLoader, RowNodeCache, SortController, RowBounds, GridApi, ColumnApi, RowDataChangedEvent, PreDestroy } from "ag-grid-community"; import {ServerSideCache, ServerSideCacheParams} from "./serverSideCache"; @Bean('rowModel') export class ServerSideRowModel extends BeanStub implements IServerSideRowModel { @Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper; @Autowired('eventService') private eventService: EventService; @Autowired('context') private context: Context; @Autowired('columnController') private columnController: ColumnController; @Autowired('filterManager') private filterManager: FilterManager; @Autowired('sortController') private sortController: SortController; @Autowired('gridApi') private gridApi: GridApi; @Autowired('columnApi') private columnApi: ColumnApi; private rootNode: RowNode; private datasource: IServerSideDatasource; private rowHeight: number; private cacheParams: ServerSideCacheParams; private logger: Logger; private rowNodeBlockLoader: RowNodeBlockLoader; @PostConstruct private postConstruct(): void { this.rowHeight = this.gridOptionsWrapper.getRowHeightAsNumber(); this.addEventListeners(); let datasource = this.gridOptionsWrapper.getServerSideDatasource(); if (_.exists(datasource)) { this.setDatasource(datasource); } } @PreDestroy public destroy(): void { super.destroy(); } @PreDestroy private destroyDatasource(): void { if (this.datasource && this.datasource.destroy) { this.datasource.destroy(); } this.datasource = null; } private setBeans(@Qualifier('loggerFactory') loggerFactory: LoggerFactory) { this.logger = loggerFactory.create('ServerSideRowModel'); } private addEventListeners(): void { this.addDestroyableEventListener(this.eventService, Events.EVENT_COLUMN_ROW_GROUP_CHANGED, this.onColumnRowGroupChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_ROW_GROUP_OPENED, this.onRowGroupOpened.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_COLUMN_PIVOT_MODE_CHANGED, this.onPivotModeChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_COLUMN_EVERYTHING_CHANGED, this.onColumnEverything.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_COLUMN_VALUE_CHANGED, this.onValueChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_COLUMN_PIVOT_CHANGED, this.onColumnPivotChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_FILTER_CHANGED, this.onFilterChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_SORT_CHANGED, this.onSortChanged.bind(this)); } public setDatasource(datasource: IServerSideDatasource): void { this.destroyDatasource(); this.datasource = datasource; this.reset(); } public isLastRowFound(): boolean { if (this.cacheExists()) { return this.rootNode.childrenCache.isMaxRowFound(); } else { return false; } } private onColumnEverything(): void { // this is a hack for one customer only, so they can suppress the resetting of the columns. // The problem the customer had was they were api.setColumnDefs() after the data source came // back with data. So this stops the reload from the grid after the data comes back. // Once we have "AG-1591 Allow delta changes to columns" fixed, then this hack can be taken out. if (this.gridOptionsWrapper.isSuppressEnterpriseResetOnNewColumns()) { return; } // every other customer can continue as normal and have it working!!! // check if anything pertaining to fetching data has changed, and if it has, reset, but if // it has not, don't reset let resetRequired: boolean; if (!this.cacheParams) { resetRequired = true; } else { let rowGroupColumnVos = this.toValueObjects(this.columnController.getRowGroupColumns()); let valueColumnVos = this.toValueObjects(this.columnController.getValueColumns()); let pivotColumnVos = this.toValueObjects(this.columnController.getPivotColumns()); let sortModelDifferent = !_.jsonEquals(this.cacheParams.sortModel, this.sortController.getSortModel()); let rowGroupDifferent = !_.jsonEquals(this.cacheParams.rowGroupCols, rowGroupColumnVos); let pivotDifferent = !_.jsonEquals(this.cacheParams.pivotCols, pivotColumnVos); let valuesDifferent = !_.jsonEquals(this.cacheParams.valueCols, valueColumnVos); resetRequired = sortModelDifferent || rowGroupDifferent || pivotDifferent || valuesDifferent; } if (resetRequired) { this.reset(); } } private onFilterChanged(): void { this.reset(); } // returns back all the cols that were effected by the sorting. eg if we were sorting by col A, // and now we are sorting by col B, the list of impacted cols should be A and B. so if a cache // is impacted by sorting on A or B then it needs to be refreshed. this includes where the cache // was previously sorted by A and then the A sort now needs to be cleared. private findChangedColumnsInSort( newSortModel: {colId: string, sort: string}[], oldSortModel: {colId: string, sort: string}[]): string[] { let allColsInBothSorts: string[] = []; [newSortModel, oldSortModel].forEach( sortModel => { if (sortModel) { let ids = sortModel.map(sm => sm.colId); allColsInBothSorts = allColsInBothSorts.concat(ids); } }); let differentSorts = (oldSortItem: any, newSortItem: any) => { let oldSort = oldSortItem ? oldSortItem.sort : null; let newSort = newSortItem ? newSortItem.sort : null; return oldSort !== newSort; }; let differentIndexes = (oldSortItem: any, newSortItem: any) => { let oldIndex = oldSortModel.indexOf(oldSortItem); let newIndex = newSortModel.indexOf(newSortItem); return oldIndex !== newIndex; }; return allColsInBothSorts.filter( colId => { let oldSortItem = _.find(oldSortModel, sm => sm.colId === colId); let newSortItem = _.find(newSortModel, sm => sm.colId === colId); return differentSorts(oldSortItem, newSortItem) || differentIndexes(oldSortItem, newSortItem); }); } private onSortChanged(): void { if (!this.cacheExists()) return; let newSortModel = this.extractSortModel(); let oldSortModel = this.cacheParams.sortModel; let changedColumnsInSort = this.findChangedColumnsInSort(newSortModel, oldSortModel); this.cacheParams.sortModel = newSortModel; let rowGroupColIds = this.columnController.getRowGroupColumns().map(col => col.getId()); let serverSideCache = <ServerSideCache> this.rootNode.childrenCache; let sortingWithValueCol = this.isSortingWithValueColumn(changedColumnsInSort); let sortingWithSecondaryCol = this.isSortingWithSecondaryColumn(changedColumnsInSort); let sortAlwaysResets = this.gridOptionsWrapper.isServerSideSortingAlwaysResets(); if (sortAlwaysResets || sortingWithValueCol || sortingWithSecondaryCol) { this.reset(); } else { serverSideCache.refreshCacheAfterSort(changedColumnsInSort, rowGroupColIds); } } private onValueChanged(): void { this.reset(); } private onColumnRowGroupChanged(): void { this.reset(); } private onColumnPivotChanged(): void { this.reset(); } private onPivotModeChanged(): void { this.reset(); } private onRowGroupOpened(event: any): void { let rowNode = <RowNode> event.node; if (rowNode.expanded) { if (_.missing(rowNode.childrenCache)) { this.createNodeCache(rowNode); } } else { if (this.gridOptionsWrapper.isPurgeClosedRowNodes() && _.exists(rowNode.childrenCache)) { rowNode.childrenCache.destroy(); rowNode.childrenCache = null; } } this.updateRowIndexesAndBounds(); let modelUpdatedEvent: ModelUpdatedEvent = { type: Events.EVENT_MODEL_UPDATED, api: this.gridOptionsWrapper.getApi(), columnApi: this.gridOptionsWrapper.getColumnApi(), newPage: false, newData: false, animate: true, keepRenderedRows: true }; this.eventService.dispatchEvent(modelUpdatedEvent); } private reset(): void { this.rootNode = new RowNode(); this.rootNode.group = true; this.rootNode.level = -1; this.context.wireBean(this.rootNode); if (this.datasource) { this.createNewRowNodeBlockLoader(); this.cacheParams = this.createCacheParams(); this.createNodeCache(this.rootNode); this.updateRowIndexesAndBounds(); } // this event: 1) clears selection 2) updates filters 3) shows/hides 'no rows' overlay let rowDataChangedEvent: RowDataChangedEvent = { type: Events.EVENT_ROW_DATA_CHANGED, api: this.gridApi, columnApi: this.columnApi }; this.eventService.dispatchEvent(rowDataChangedEvent); // this gets the row to render rows (or remove the previously rendered rows, as it's blank to start). // important to NOT pass in an event with keepRenderedRows or animate, as we want the renderer // to treat the rows as new rows, as it's all new data let modelUpdatedEvent: ModelUpdatedEvent = { type: Events.EVENT_MODEL_UPDATED, api: this.gridApi, columnApi: this.columnApi, animate: false, keepRenderedRows: false, newData: false, newPage: false }; this.eventService.dispatchEvent(modelUpdatedEvent); } private createNewRowNodeBlockLoader(): void { this.destroyRowNodeBlockLoader(); let maxConcurrentRequests = this.gridOptionsWrapper.getMaxConcurrentDatasourceRequests(); let blockLoadDebounceMillis = this.gridOptionsWrapper.getBlockLoadDebounceMillis(); this.rowNodeBlockLoader = new RowNodeBlockLoader(maxConcurrentRequests, blockLoadDebounceMillis); this.context.wireBean(this.rowNodeBlockLoader); } private destroyRowNodeBlockLoader(): void { if (this.rowNodeBlockLoader) { this.rowNodeBlockLoader.destroy(); this.rowNodeBlockLoader = null; } } private toValueObjects(columns: Column[]): ColumnVO[] { return columns.map( col => <ColumnVO> { id: col.getId(), aggFunc: col.getAggFunc(), displayName: this.columnController.getDisplayNameForColumn(col, 'model'), field: col.getColDef().field }); } private createCacheParams(): ServerSideCacheParams { let rowGroupColumnVos = this.toValueObjects(this.columnController.getRowGroupColumns()); let valueColumnVos = this.toValueObjects(this.columnController.getValueColumns()); let pivotColumnVos = this.toValueObjects(this.columnController.getPivotColumns()); let dynamicRowHeight = this.gridOptionsWrapper.isDynamicRowHeight(); let maxBlocksInCache = this.gridOptionsWrapper.getMaxBlocksInCache(); if (dynamicRowHeight && maxBlocksInCache >= 0 ) { console.warn('ag-Grid: Server Side Row Model does not support Dynamic Row Height and Cache Purging. ' + 'Either a) remove getRowHeight() callback or b) remove maxBlocksInCache property. Purging has been disabled.'); maxBlocksInCache = undefined; } if (maxBlocksInCache >= 0 && this.columnController.isAutoRowHeightActive()) { console.warn('ag-Grid: Server Side Row Model does not support Auto Row Height and Cache Purging. ' + 'Either a) remove colDef.autoHeight or b) remove maxBlocksInCache property. Purging has been disabled.'); maxBlocksInCache = undefined; } let params: ServerSideCacheParams = { // the columns the user has grouped and aggregated by valueCols: valueColumnVos, rowGroupCols: rowGroupColumnVos, pivotCols: pivotColumnVos, pivotMode: this.columnController.isPivotMode(), // sort and filter model filterModel: this.filterManager.getFilterModel(), sortModel: this.extractSortModel(), rowNodeBlockLoader: this.rowNodeBlockLoader, datasource: this.datasource, lastAccessedSequence: new NumberSequence(), overflowSize: 1, initialRowCount: 1, maxConcurrentRequests: this.gridOptionsWrapper.getMaxConcurrentDatasourceRequests(), maxBlocksInCache: maxBlocksInCache, blockSize: this.gridOptionsWrapper.getCacheBlockSize(), rowHeight: this.rowHeight, dynamicRowHeight: dynamicRowHeight }; // set defaults if ( !(params.maxConcurrentRequests>=1) ) { params.maxConcurrentRequests = 2; } // page size needs to be 1 or greater. having it at 1 would be silly, as you would be hitting the // server for one page at a time. so the default if not specified is 100. if ( !(params.blockSize>=1) ) { params.blockSize = 100; } // if user doesn't give initial rows to display, we assume zero if ( !(params.initialRowCount>=1) ) { params.initialRowCount = 0; } // if user doesn't provide overflow, we use default overflow of 1, so user can scroll past // the current page and request first row of next page if ( !(params.overflowSize>=1) ) { params.overflowSize = 1; } return params; } private createNodeCache(rowNode: RowNode): void { let cache = new ServerSideCache(this.cacheParams, rowNode); this.context.wireBean(cache); cache.addEventListener(RowNodeCache.EVENT_CACHE_UPDATED, this.onCacheUpdated.bind(this)); rowNode.childrenCache = cache; } private onCacheUpdated(): void { this.updateRowIndexesAndBounds(); let modelUpdatedEvent: ModelUpdatedEvent = { type: Events.EVENT_MODEL_UPDATED, api: this.gridApi, columnApi: this.columnApi, animate: true, keepRenderedRows: true, newPage: false, newData: false }; this.eventService.dispatchEvent(modelUpdatedEvent); } public updateRowIndexesAndBounds(): void { if (this.cacheExists()) { // NOTE: should not be casting here, the RowModel should use IServerSideRowModel interface? let serverSideCache = <ServerSideCache> this.rootNode.childrenCache; this.resetRowTops(serverSideCache); this.setDisplayIndexes(serverSideCache); } } private setDisplayIndexes(cache: ServerSideCache): void { let numberSequence = new NumberSequence(); let nextRowTop = {value: 0}; cache.setDisplayIndexes(numberSequence, nextRowTop); } // resetting row tops is needed for animation, as part of the operation is saving the old location, // which is needed for rows that are transitioning in private resetRowTops(cache: ServerSideCache): void { let numberSequence = new NumberSequence(); cache.forEachNodeDeep( rowNode => rowNode.clearRowTop(), numberSequence); } public getRow(index: number): RowNode { if (this.cacheExists()) { return this.rootNode.childrenCache.getRow(index); } else { return null; } } public getPageFirstRow(): number { return 0; } public getPageLastRow(): number { let lastRow: number; if (this.cacheExists()) { // NOTE: should not be casting here, the RowModel should use IServerSideRowModel interface? let serverSideCache = <ServerSideCache> this.rootNode.childrenCache; lastRow = serverSideCache.getDisplayIndexEnd() - 1; } else { lastRow = 0; } return lastRow; } public getRowCount(): number { return this.getPageLastRow() + 1; } public getRowBounds(index: number): RowBounds { if (!this.cacheExists()) { return { rowTop: 0, rowHeight: this.rowHeight }; } let serverSideCache = <ServerSideCache> this.rootNode.childrenCache; return serverSideCache.getRowBounds(index); } public getRowIndexAtPixel(pixel: number): number { if (pixel === 0) return 0; if (!this.cacheExists()) return 0; let serverSideCache = <ServerSideCache> this.rootNode.childrenCache; return serverSideCache.getRowIndexAtPixel(pixel); } public getCurrentPageHeight(): number { return this.rowHeight * this.getRowCount(); } public isEmpty(): boolean { return false; } public isRowsToRender(): boolean { return this.cacheExists() && this.getRowCount() > 0; } public getType(): string { return Constants.ROW_MODEL_TYPE_SERVER_SIDE; } public forEachNode(callback: (rowNode: RowNode, index: number)=>void): void { if (this.cacheExists()) { this.rootNode.childrenCache.forEachNodeDeep(callback, new NumberSequence()); } } private executeOnCache(route: string[], callback: (cache: ServerSideCache)=>void) { if (this.cacheExists()) { let topLevelCache = <ServerSideCache> this.rootNode.childrenCache; let cacheToPurge = topLevelCache.getChildCache(route); if (cacheToPurge) { callback(cacheToPurge); } } } public purgeCache(route: string[] = []): void { this.executeOnCache(route, cache => cache.purgeCache() ); } public removeFromCache(route: string[], items: any[]): void { this.executeOnCache(route, cache => cache.removeFromCache(items) ); this.rowNodeBlockLoader.checkBlockToLoad(); } public addToCache(route: string[], items: any[], index: number): void { this.executeOnCache(route, cache => cache.addToCache(items, index) ); } public getNodesInRangeForSelection(firstInRange: RowNode, lastInRange: RowNode): RowNode[] { if (_.exists(firstInRange) && firstInRange.parent !== lastInRange.parent) return []; return lastInRange.parent.childrenCache.getRowNodesInRange(firstInRange, lastInRange); } public getRowNode(id: string): RowNode { let result: RowNode = null; this.forEachNode(rowNode => { if(rowNode.id === id) { result = rowNode; } }); return result; } public getBlockState(): any { if (this.rowNodeBlockLoader) { return this.rowNodeBlockLoader.getBlockState(); } else { return null; } } // always returns true - this is used by the public isRowPresent(rowNode: RowNode): boolean { let foundRowNode = this.getRowNode(rowNode.id); return !!foundRowNode; } private extractSortModel(): { colId: string; sort: string }[] { let sortModel = this.sortController.getSortModel(); // when using tree data we just return the sort model with the 'ag-Grid-AutoColumn' as is, i.e not broken out // into it's constitute group columns as they are not defined up front and can vary per node. if (this.gridOptionsWrapper.isTreeData()) { return sortModel; } let rowGroupCols = this.toValueObjects(this.columnController.getRowGroupColumns()); // find index of auto group column in sort model let autoGroupIndex = -1; for (let i = 0; i < sortModel.length; ++i) { if (sortModel[i].colId === 'ag-Grid-AutoColumn') { autoGroupIndex = i; break; } } // replace auto column with individual group columns if (autoGroupIndex > -1) { let individualGroupCols = rowGroupCols.map(group => { return { colId: group.field, sort: sortModel[autoGroupIndex].sort }; }); // remove auto group column sortModel.splice(autoGroupIndex, 1); // insert individual group columns for (let i = 0; i < individualGroupCols.length; i++) { let individualGroupCol = individualGroupCols[i]; // don't add individual group column if non group column already exists as it gets precedence let sameNonGroupColumnExists = sortModel.some(sm => sm.colId === individualGroupCol.colId); if (sameNonGroupColumnExists) continue; sortModel.splice(autoGroupIndex++, 0, individualGroupCol); } } return sortModel; } private isSortingWithValueColumn(changedColumnsInSort: string[]): boolean { let valueColIds = this.columnController.getValueColumns().map(col => col.getColId()); for (let i = 0; i < changedColumnsInSort.length; i++) { if (valueColIds.indexOf(changedColumnsInSort[i]) > -1) { return true; } } return false; } private isSortingWithSecondaryColumn(changedColumnsInSort: string[]): boolean { if (!this.columnController.getSecondaryColumns()) return false; let secondaryColIds = this.columnController.getSecondaryColumns().map(col => col.getColId()); for (let i = 0; i < changedColumnsInSort.length; i++) { if (secondaryColIds.indexOf(changedColumnsInSort[i]) > -1) { return true; } } return false; } private cacheExists(): boolean { return _.exists(this.rootNode) && _.exists(this.rootNode.childrenCache); } }