From 9b14ee0958150ac928af52ad6c58eff9761d1b2b Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Thu, 11 Apr 2024 09:49:44 -0400 Subject: [PATCH] feat: improve table loading (#1898) - Adds #1865 - Add a check for if there is still data being loaded in the viewport - Add a new loading message if the above is true for >500ms - Add state to determine whether `startLoading` will block the grid or show the cancel button --- packages/iris-grid/src/IrisGrid.scss | 1 - packages/iris-grid/src/IrisGrid.tsx | 93 ++++++++++++++++--- packages/iris-grid/src/IrisGridModel.ts | 9 ++ packages/iris-grid/src/IrisGridProxyModel.ts | 4 + .../src/IrisGridTableModelTemplate.ts | 38 ++++++++ 5 files changed, 129 insertions(+), 16 deletions(-) diff --git a/packages/iris-grid/src/IrisGrid.scss b/packages/iris-grid/src/IrisGrid.scss index 2e6428ccba..a6e71616de 100644 --- a/packages/iris-grid/src/IrisGrid.scss +++ b/packages/iris-grid/src/IrisGrid.scss @@ -154,7 +154,6 @@ $cell-invalid-box-shadow: .iris-grid-loading { position: absolute; - top: 0; bottom: 0; left: 0; right: 0; diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index 749efe9400..3bdf4edce9 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -191,6 +191,8 @@ import { isMissingPartitionError } from './MissingPartitionError'; const log = Log.module('IrisGrid'); +const VIEWPORT_LOADING_DELAY = 500; + const UPDATE_DOWNLOAD_THROTTLE = 500; const SET_FILTER_DEBOUNCE = 250; @@ -379,6 +381,8 @@ export interface IrisGridState { loadingText: string | null; loadingScrimProgress: number | null; loadingSpinnerShown: boolean; + loadingCancelShown: boolean; + loadingBlocksGrid: boolean; movedColumns: readonly MoveOperation[]; movedRows: readonly MoveOperation[]; @@ -579,6 +583,7 @@ class IrisGrid extends Component { this.handleSelectDistinctChanged = this.handleSelectDistinctChanged.bind(this); this.handlePendingDataUpdated = this.handlePendingDataUpdated.bind(this); + this.handleViewportUpdated = this.handleViewportUpdated.bind(this); this.handlePendingCommitClicked = this.handlePendingCommitClicked.bind(this); this.handlePendingDiscardClicked = @@ -608,6 +613,7 @@ class IrisGrid extends Component { this.handleGotoValueSelectedFilterChanged.bind(this); this.handleGotoValueChanged = this.handleGotoValueChanged.bind(this); this.handleGotoValueSubmitted = this.handleGotoValueSubmitted.bind(this); + this.handleViewportUpdated = this.handleViewportUpdated.bind(this); this.makeQuickFilter = this.makeQuickFilter.bind(this); this.grid = null; @@ -796,6 +802,8 @@ class IrisGrid extends Component { loadingText: null, loadingScrimProgress: null, loadingSpinnerShown: false, + loadingCancelShown: false, + loadingBlocksGrid: false, movedColumns, movedRows, @@ -897,7 +905,7 @@ class IrisGrid extends Component { this.clearGridInputField(); this.clearCrossColumSearch(); } - this.startLoading('Filtering...', true); + this.startLoading('Filtering...', { resetRanges: true }); this.applyInputFilters(changedInputFilters, replaceExistingFilters); } @@ -908,7 +916,7 @@ class IrisGrid extends Component { this.updateFormatterSettings(settings); } if (customFilters !== prevProps.customFilters) { - this.startLoading('Filtering...', true); + this.startLoading('Filtering...', { resetRanges: true }); } if (sorts !== prevProps.sorts) { this.updateSorts(sorts); @@ -964,6 +972,8 @@ class IrisGrid extends Component { if (this.animationFrame !== undefined) { cancelAnimationFrame(this.animationFrame); } + + this.showViewportLoading.cancel(); } grid: Grid | null; @@ -1591,7 +1601,7 @@ class IrisGrid extends Component { ): void { log.debug('Setting advanced filter', modelIndex, filter); - this.startLoading('Filtering...', true); + this.startLoading('Filtering...', { resetRanges: true }); this.setState(({ advancedFilters }) => { const newAdvancedFilters = new Map(advancedFilters); @@ -1620,7 +1630,7 @@ class IrisGrid extends Component { ): void { log.debug('Setting quick filter', modelIndex, filter, text); - this.startLoading('Filtering...', true); + this.startLoading('Filtering...', { resetRanges: true }); this.setState(({ quickFilters }) => { const newQuickFilters = new Map(quickFilters); @@ -1673,7 +1683,7 @@ class IrisGrid extends Component { } removeColumnFilter(modelRange: ModelIndex | BoundedAxisRange): void { - this.startLoading('Filtering...', true); + this.startLoading('Filtering...', { resetRanges: true }); const clearRange: BoundedAxisRange = Array.isArray(modelRange) ? modelRange @@ -1707,7 +1717,7 @@ class IrisGrid extends Component { } removeQuickFilter(modelColumn: ModelIndex): void { - this.startLoading('Clearing Filter...', true); + this.startLoading('Clearing Filter...', { resetRanges: true }); this.setState(({ quickFilters }) => { const newQuickFilters = new Map(quickFilters); @@ -1732,7 +1742,7 @@ class IrisGrid extends Component { // if there is an active quick filter input field, reset it as well this.clearGridInputField(); - this.startLoading('Clearing Filters...', true); + this.startLoading('Clearing Filters...', { resetRanges: true }); this.setState({ quickFilters: new Map(), advancedFilters: new Map(), @@ -1792,7 +1802,7 @@ class IrisGrid extends Component { }); }); - this.startLoading('Rebuilding filters...', true); + this.startLoading('Rebuilding filters...', { resetRanges: true }); this.setState({ quickFilters: newQuickFilters, advancedFilters: newAdvancedFilters, @@ -2140,8 +2150,15 @@ class IrisGrid extends Component { } } - startLoading(loadingText: string, resetRanges = false): void { - this.setState({ loadingText }); + startLoading( + loadingText: string, + { + resetRanges = false, + loadingCancelShown = true, + loadingBlocksGrid = true, + } = {} + ): void { + this.setState({ loadingText, loadingCancelShown, loadingBlocksGrid }); const theme = this.getTheme(); @@ -2174,12 +2191,14 @@ class IrisGrid extends Component { } stopLoading(): void { + this.showViewportLoading.cancel(); this.loadingScrimStartTime = undefined; this.loadingScrimFinishTime = undefined; this.setState({ loadingText: null, loadingScrimProgress: null, loadingSpinnerShown: false, + loadingCancelShown: false, }); if (this.loadingTimer != null) { @@ -2255,6 +2274,10 @@ class IrisGrid extends Component { IrisGridModel.EVENT.PENDING_DATA_UPDATED, this.handlePendingDataUpdated ); + model.addEventListener( + IrisGridModel.EVENT.VIEWPORT_UPDATED, + this.handleViewportUpdated + ); } stopListening(model: IrisGridModel): void { @@ -2271,6 +2294,10 @@ class IrisGrid extends Component { IrisGridModel.EVENT.PENDING_DATA_UPDATED, this.handlePendingDataUpdated ); + model.removeEventListener( + IrisGridModel.EVENT.VIEWPORT_UPDATED, + this.handleViewportUpdated + ); } focus(): void { @@ -2504,6 +2531,37 @@ class IrisGrid extends Component { onError(error); } + handleViewportUpdated(): void { + const { model } = this.props; + const { loadingText, loadingSpinnerShown } = this.state; + const loadingMessage = 'Waiting for viewport...'; + + // pending and no timer already exists + if (model.isViewportPending && !loadingSpinnerShown) { + this.showViewportLoading(); + } else if (loadingText === loadingMessage && !model.isViewportPending) { + // extra conditions because timeout might get cleared by update + this.stopLoading(); + } + } + + showViewportLoading = throttle( + (): void => { + const { model } = this.props; + const { loadingSpinnerShown } = this.state; + if (model.isViewportPending && !loadingSpinnerShown) { + // We only want to show the viewport loading if the viewport is still loading + // and we're not already showing a loader for something else + this.startLoading('Waiting for viewport...', { + loadingCancelShown: false, + loadingBlocksGrid: false, + }); + } + }, + VIEWPORT_LOADING_DELAY, + { leading: false, trailing: true } + ); + showAllColumns(): void { const { metricCalculator } = this.state; const userColumnWidths = metricCalculator.getUserColumnWidths(); @@ -2891,7 +2949,7 @@ class IrisGrid extends Component { } handleFilterBarChange(value: string): void { - this.startLoading('Filtering...', true); + this.startLoading('Filtering...', { resetRanges: true }); this.setState(({ focusedFilterBarColumn, quickFilters }) => { const newQuickFilters = new Map(quickFilters); @@ -2979,12 +3037,12 @@ class IrisGrid extends Component { const { partitionConfig } = this.state; if (isMissingPartitionError(error) && partitionConfig != null) { // We'll try loading the initial partition again - this.startLoading('Reloading partition...', true); + this.startLoading('Reloading partition...', { resetRanges: true }); this.setState({ partitionConfig: undefined }, () => { this.initState(); }); } else if (this.canRollback()) { - this.startLoading('Rolling back changes...', true); + this.startLoading('Rolling back changes...', { resetRanges: true }); this.rollback(); } else { log.error('Table failed and unable to rollback'); @@ -4089,6 +4147,8 @@ class IrisGrid extends Component { loadingText, loadingScrimProgress, loadingSpinnerShown, + loadingCancelShown, + loadingBlocksGrid, shownColumnTooltip, hoverAdvancedFilter, shownAdvancedFilter, @@ -4261,7 +4321,7 @@ class IrisGrid extends Component { type="button" onClick={this.handleCancel} className={classNames('iris-grid-btn-cancel', { - show: loadingSpinnerShown, + show: loadingCancelShown, })} > @@ -4271,7 +4331,10 @@ class IrisGrid extends Component { ); const gridY = metrics ? metrics.gridY : 0; loadingElement = ( -
+
{loadingStatus}
); diff --git a/packages/iris-grid/src/IrisGridModel.ts b/packages/iris-grid/src/IrisGridModel.ts index 71f14448bc..daf66a0391 100644 --- a/packages/iris-grid/src/IrisGridModel.ts +++ b/packages/iris-grid/src/IrisGridModel.ts @@ -74,7 +74,9 @@ abstract class IrisGridModel< DISCONNECT: 'DISCONNECT', RECONNECT: 'RECONNECT', TOTALS_UPDATED: 'TOTALS_UPDATED', + /** Fired when the viewport is applied to the table and we're waiting for a response. */ PENDING_DATA_UPDATED: 'PENDING_DATA_UPDATED', + VIEWPORT_UPDATED: 'VIEWPORT_UPDATED', } as const); constructor(dh: typeof DhType) { @@ -484,6 +486,13 @@ abstract class IrisGridModel< */ abstract commitPending(): Promise; + /** + * Check if viewport is still loading data + */ + get isViewportPending(): boolean { + return false; + } + /** * Check if a column is filterable * @param columnIndex The column index to check for filterability diff --git a/packages/iris-grid/src/IrisGridProxyModel.ts b/packages/iris-grid/src/IrisGridProxyModel.ts index 57ab05c0d9..a934d38c1a 100644 --- a/packages/iris-grid/src/IrisGridProxyModel.ts +++ b/packages/iris-grid/src/IrisGridProxyModel.ts @@ -697,6 +697,10 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel { return isEditableGridModel(this.model) && this.model.isEditable; } + get isViewportPending(): boolean { + return this.model.isViewportPending; + } + isEditableRange: IrisGridTableModel['isEditableRange'] = ( ...args ): boolean => { diff --git a/packages/iris-grid/src/IrisGridTableModelTemplate.ts b/packages/iris-grid/src/IrisGridTableModelTemplate.ts index 3f8b1db06d..452b715342 100644 --- a/packages/iris-grid/src/IrisGridTableModelTemplate.ts +++ b/packages/iris-grid/src/IrisGridTableModelTemplate.ts @@ -455,6 +455,41 @@ class IrisGridTableModelTemplate< return !this.isSaveInProgress && this.inputTable != null; } + get isViewportPending(): boolean { + if ( + this.viewport == null || + this.viewport.columns === undefined || + this.viewportData == null + ) { + return true; + } + // no columns or no rows + if ( + this.viewport.columns.length === 0 || + this.viewportData.rows.length === 0 + ) { + return false; + } + + // offset is first row of loaded data + const pendingTop = this.viewport.top < this.viewportData.offset; + // offset + row.length is last row of loaded data + const pendingBottom = + this.viewportData.offset + this.viewportData.rows.length < + this.viewport.bottom; + // left column doesn't exist in data + const pendingLeft = + this.viewportData.rows[0].data.get(this.viewport.columns[0].index) === + undefined; + // right column doesn't exist in data + const pendingRight = + this.viewportData.rows[0].data.get( + this.viewport.columns[this.viewport.columns.length - 1].index + ) === undefined; + + return pendingTop || pendingBottom || pendingLeft || pendingRight; + } + cacheFormattedValue(x: ModelIndex, y: ModelIndex, text: string | null): void { if (this.formattedStringData[x] == null) { this.formattedStringData[x] = []; @@ -1311,6 +1346,9 @@ class IrisGridTableModelTemplate< viewportBottom: number, columns?: DhType.Column[] ): void { + this.dispatchEvent( + new EventShimCustomEvent(IrisGridModel.EVENT.VIEWPORT_UPDATED) + ); log.debug2('applyBufferedViewport', viewportTop, viewportBottom, columns); if (this.subscription == null) { log.debug2('applyBufferedViewport creating new subscription');