Skip to content

Commit

Permalink
feat: improve table loading (#1898)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
wusteven815 authored Apr 11, 2024
1 parent d3fb28a commit 9b14ee0
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 16 deletions.
1 change: 0 additions & 1 deletion packages/iris-grid/src/IrisGrid.scss
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ $cell-invalid-box-shadow:

.iris-grid-loading {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
Expand Down
93 changes: 78 additions & 15 deletions packages/iris-grid/src/IrisGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -579,6 +583,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
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 =
Expand Down Expand Up @@ -608,6 +613,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
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;
Expand Down Expand Up @@ -796,6 +802,8 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
loadingText: null,
loadingScrimProgress: null,
loadingSpinnerShown: false,
loadingCancelShown: false,
loadingBlocksGrid: false,

movedColumns,
movedRows,
Expand Down Expand Up @@ -897,7 +905,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
this.clearGridInputField();
this.clearCrossColumSearch();
}
this.startLoading('Filtering...', true);
this.startLoading('Filtering...', { resetRanges: true });
this.applyInputFilters(changedInputFilters, replaceExistingFilters);
}

Expand All @@ -908,7 +916,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
this.updateFormatterSettings(settings);
}
if (customFilters !== prevProps.customFilters) {
this.startLoading('Filtering...', true);
this.startLoading('Filtering...', { resetRanges: true });
}
if (sorts !== prevProps.sorts) {
this.updateSorts(sorts);
Expand Down Expand Up @@ -964,6 +972,8 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
if (this.animationFrame !== undefined) {
cancelAnimationFrame(this.animationFrame);
}

this.showViewportLoading.cancel();
}

grid: Grid | null;
Expand Down Expand Up @@ -1591,7 +1601,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
): 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);
Expand Down Expand Up @@ -1620,7 +1630,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
): 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);
Expand Down Expand Up @@ -1673,7 +1683,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
}

removeColumnFilter(modelRange: ModelIndex | BoundedAxisRange): void {
this.startLoading('Filtering...', true);
this.startLoading('Filtering...', { resetRanges: true });

const clearRange: BoundedAxisRange = Array.isArray(modelRange)
? modelRange
Expand Down Expand Up @@ -1707,7 +1717,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
}

removeQuickFilter(modelColumn: ModelIndex): void {
this.startLoading('Clearing Filter...', true);
this.startLoading('Clearing Filter...', { resetRanges: true });

this.setState(({ quickFilters }) => {
const newQuickFilters = new Map(quickFilters);
Expand All @@ -1732,7 +1742,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
// 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(),
Expand Down Expand Up @@ -1792,7 +1802,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
});
});

this.startLoading('Rebuilding filters...', true);
this.startLoading('Rebuilding filters...', { resetRanges: true });
this.setState({
quickFilters: newQuickFilters,
advancedFilters: newAdvancedFilters,
Expand Down Expand Up @@ -2140,8 +2150,15 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
}
}

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();

Expand Down Expand Up @@ -2174,12 +2191,14 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
}

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) {
Expand Down Expand Up @@ -2255,6 +2274,10 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
IrisGridModel.EVENT.PENDING_DATA_UPDATED,
this.handlePendingDataUpdated
);
model.addEventListener(
IrisGridModel.EVENT.VIEWPORT_UPDATED,
this.handleViewportUpdated
);
}

stopListening(model: IrisGridModel): void {
Expand All @@ -2271,6 +2294,10 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
IrisGridModel.EVENT.PENDING_DATA_UPDATED,
this.handlePendingDataUpdated
);
model.removeEventListener(
IrisGridModel.EVENT.VIEWPORT_UPDATED,
this.handleViewportUpdated
);
}

focus(): void {
Expand Down Expand Up @@ -2504,6 +2531,37 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
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();
Expand Down Expand Up @@ -2891,7 +2949,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
}

handleFilterBarChange(value: string): void {
this.startLoading('Filtering...', true);
this.startLoading('Filtering...', { resetRanges: true });

this.setState(({ focusedFilterBarColumn, quickFilters }) => {
const newQuickFilters = new Map(quickFilters);
Expand Down Expand Up @@ -2979,12 +3037,12 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
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');
Expand Down Expand Up @@ -4089,6 +4147,8 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
loadingText,
loadingScrimProgress,
loadingSpinnerShown,
loadingCancelShown,
loadingBlocksGrid,
shownColumnTooltip,
hoverAdvancedFilter,
shownAdvancedFilter,
Expand Down Expand Up @@ -4261,7 +4321,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
type="button"
onClick={this.handleCancel}
className={classNames('iris-grid-btn-cancel', {
show: loadingSpinnerShown,
show: loadingCancelShown,
})}
>
<FontAwesomeIcon icon={vsClose} transform="down-1" />
Expand All @@ -4271,7 +4331,10 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
);
const gridY = metrics ? metrics.gridY : 0;
loadingElement = (
<div className="iris-grid-loading" style={{ top: gridY }}>
<div
className="iris-grid-loading"
style={loadingBlocksGrid ? { top: gridY } : {}}
>
{loadingStatus}
</div>
);
Expand Down
9 changes: 9 additions & 0 deletions packages/iris-grid/src/IrisGridModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -484,6 +486,13 @@ abstract class IrisGridModel<
*/
abstract commitPending(): Promise<void>;

/**
* 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
Expand Down
4 changes: 4 additions & 0 deletions packages/iris-grid/src/IrisGridProxyModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
38 changes: 38 additions & 0 deletions packages/iris-grid/src/IrisGridTableModelTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [];
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit 9b14ee0

Please sign in to comment.