ag-grid-enterprise
Version:
ag-Grid Enterprise Features
624 lines (515 loc) • 23.2 kB
text/typescript
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";
export class ServerSideRowModel extends BeanStub implements IServerSideRowModel {
private gridOptionsWrapper: GridOptionsWrapper;
private eventService: EventService;
private context: Context;
private columnController: ColumnController;
private filterManager: FilterManager;
private sortController: SortController;
private gridApi: GridApi;
private columnApi: ColumnApi;
private rootNode: RowNode;
private datasource: IServerSideDatasource;
private rowHeight: number;
private cacheParams: ServerSideCacheParams;
private logger: Logger;
private rowNodeBlockLoader: RowNodeBlockLoader;
private postConstruct(): void {
this.rowHeight = this.gridOptionsWrapper.getRowHeightAsNumber();
this.addEventListeners();
let datasource = this.gridOptionsWrapper.getServerSideDatasource();
if (_.exists(datasource)) {
this.setDatasource(datasource);
}
}
public destroy(): void {
super.destroy();
}
private destroyDatasource(): void {
if (this.datasource && this.datasource.destroy) {
this.datasource.destroy();
}
this.datasource = null;
}
private setBeans( 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);
}
}