Skip to content

Commit 9b14ee0

Browse files
authored
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
1 parent d3fb28a commit 9b14ee0

5 files changed

Lines changed: 129 additions & 16 deletions

File tree

packages/iris-grid/src/IrisGrid.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ $cell-invalid-box-shadow:
154154

155155
.iris-grid-loading {
156156
position: absolute;
157-
top: 0;
158157
bottom: 0;
159158
left: 0;
160159
right: 0;

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ import { isMissingPartitionError } from './MissingPartitionError';
191191

192192
const log = Log.module('IrisGrid');
193193

194+
const VIEWPORT_LOADING_DELAY = 500;
195+
194196
const UPDATE_DOWNLOAD_THROTTLE = 500;
195197

196198
const SET_FILTER_DEBOUNCE = 250;
@@ -379,6 +381,8 @@ export interface IrisGridState {
379381
loadingText: string | null;
380382
loadingScrimProgress: number | null;
381383
loadingSpinnerShown: boolean;
384+
loadingCancelShown: boolean;
385+
loadingBlocksGrid: boolean;
382386

383387
movedColumns: readonly MoveOperation[];
384388
movedRows: readonly MoveOperation[];
@@ -579,6 +583,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
579583
this.handleSelectDistinctChanged =
580584
this.handleSelectDistinctChanged.bind(this);
581585
this.handlePendingDataUpdated = this.handlePendingDataUpdated.bind(this);
586+
this.handleViewportUpdated = this.handleViewportUpdated.bind(this);
582587
this.handlePendingCommitClicked =
583588
this.handlePendingCommitClicked.bind(this);
584589
this.handlePendingDiscardClicked =
@@ -608,6 +613,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
608613
this.handleGotoValueSelectedFilterChanged.bind(this);
609614
this.handleGotoValueChanged = this.handleGotoValueChanged.bind(this);
610615
this.handleGotoValueSubmitted = this.handleGotoValueSubmitted.bind(this);
616+
this.handleViewportUpdated = this.handleViewportUpdated.bind(this);
611617
this.makeQuickFilter = this.makeQuickFilter.bind(this);
612618

613619
this.grid = null;
@@ -796,6 +802,8 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
796802
loadingText: null,
797803
loadingScrimProgress: null,
798804
loadingSpinnerShown: false,
805+
loadingCancelShown: false,
806+
loadingBlocksGrid: false,
799807

800808
movedColumns,
801809
movedRows,
@@ -897,7 +905,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
897905
this.clearGridInputField();
898906
this.clearCrossColumSearch();
899907
}
900-
this.startLoading('Filtering...', true);
908+
this.startLoading('Filtering...', { resetRanges: true });
901909
this.applyInputFilters(changedInputFilters, replaceExistingFilters);
902910
}
903911

@@ -908,7 +916,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
908916
this.updateFormatterSettings(settings);
909917
}
910918
if (customFilters !== prevProps.customFilters) {
911-
this.startLoading('Filtering...', true);
919+
this.startLoading('Filtering...', { resetRanges: true });
912920
}
913921
if (sorts !== prevProps.sorts) {
914922
this.updateSorts(sorts);
@@ -964,6 +972,8 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
964972
if (this.animationFrame !== undefined) {
965973
cancelAnimationFrame(this.animationFrame);
966974
}
975+
976+
this.showViewportLoading.cancel();
967977
}
968978

969979
grid: Grid | null;
@@ -1591,7 +1601,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
15911601
): void {
15921602
log.debug('Setting advanced filter', modelIndex, filter);
15931603

1594-
this.startLoading('Filtering...', true);
1604+
this.startLoading('Filtering...', { resetRanges: true });
15951605

15961606
this.setState(({ advancedFilters }) => {
15971607
const newAdvancedFilters = new Map(advancedFilters);
@@ -1620,7 +1630,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
16201630
): void {
16211631
log.debug('Setting quick filter', modelIndex, filter, text);
16221632

1623-
this.startLoading('Filtering...', true);
1633+
this.startLoading('Filtering...', { resetRanges: true });
16241634

16251635
this.setState(({ quickFilters }) => {
16261636
const newQuickFilters = new Map(quickFilters);
@@ -1673,7 +1683,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
16731683
}
16741684

16751685
removeColumnFilter(modelRange: ModelIndex | BoundedAxisRange): void {
1676-
this.startLoading('Filtering...', true);
1686+
this.startLoading('Filtering...', { resetRanges: true });
16771687

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

17091719
removeQuickFilter(modelColumn: ModelIndex): void {
1710-
this.startLoading('Clearing Filter...', true);
1720+
this.startLoading('Clearing Filter...', { resetRanges: true });
17111721

17121722
this.setState(({ quickFilters }) => {
17131723
const newQuickFilters = new Map(quickFilters);
@@ -1732,7 +1742,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
17321742
// if there is an active quick filter input field, reset it as well
17331743
this.clearGridInputField();
17341744

1735-
this.startLoading('Clearing Filters...', true);
1745+
this.startLoading('Clearing Filters...', { resetRanges: true });
17361746
this.setState({
17371747
quickFilters: new Map(),
17381748
advancedFilters: new Map(),
@@ -1792,7 +1802,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
17921802
});
17931803
});
17941804

1795-
this.startLoading('Rebuilding filters...', true);
1805+
this.startLoading('Rebuilding filters...', { resetRanges: true });
17961806
this.setState({
17971807
quickFilters: newQuickFilters,
17981808
advancedFilters: newAdvancedFilters,
@@ -2140,8 +2150,15 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
21402150
}
21412151
}
21422152

2143-
startLoading(loadingText: string, resetRanges = false): void {
2144-
this.setState({ loadingText });
2153+
startLoading(
2154+
loadingText: string,
2155+
{
2156+
resetRanges = false,
2157+
loadingCancelShown = true,
2158+
loadingBlocksGrid = true,
2159+
} = {}
2160+
): void {
2161+
this.setState({ loadingText, loadingCancelShown, loadingBlocksGrid });
21452162

21462163
const theme = this.getTheme();
21472164

@@ -2174,12 +2191,14 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
21742191
}
21752192

21762193
stopLoading(): void {
2194+
this.showViewportLoading.cancel();
21772195
this.loadingScrimStartTime = undefined;
21782196
this.loadingScrimFinishTime = undefined;
21792197
this.setState({
21802198
loadingText: null,
21812199
loadingScrimProgress: null,
21822200
loadingSpinnerShown: false,
2201+
loadingCancelShown: false,
21832202
});
21842203

21852204
if (this.loadingTimer != null) {
@@ -2255,6 +2274,10 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
22552274
IrisGridModel.EVENT.PENDING_DATA_UPDATED,
22562275
this.handlePendingDataUpdated
22572276
);
2277+
model.addEventListener(
2278+
IrisGridModel.EVENT.VIEWPORT_UPDATED,
2279+
this.handleViewportUpdated
2280+
);
22582281
}
22592282

22602283
stopListening(model: IrisGridModel): void {
@@ -2271,6 +2294,10 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
22712294
IrisGridModel.EVENT.PENDING_DATA_UPDATED,
22722295
this.handlePendingDataUpdated
22732296
);
2297+
model.removeEventListener(
2298+
IrisGridModel.EVENT.VIEWPORT_UPDATED,
2299+
this.handleViewportUpdated
2300+
);
22742301
}
22752302

22762303
focus(): void {
@@ -2504,6 +2531,37 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
25042531
onError(error);
25052532
}
25062533

2534+
handleViewportUpdated(): void {
2535+
const { model } = this.props;
2536+
const { loadingText, loadingSpinnerShown } = this.state;
2537+
const loadingMessage = 'Waiting for viewport...';
2538+
2539+
// pending and no timer already exists
2540+
if (model.isViewportPending && !loadingSpinnerShown) {
2541+
this.showViewportLoading();
2542+
} else if (loadingText === loadingMessage && !model.isViewportPending) {
2543+
// extra conditions because timeout might get cleared by update
2544+
this.stopLoading();
2545+
}
2546+
}
2547+
2548+
showViewportLoading = throttle(
2549+
(): void => {
2550+
const { model } = this.props;
2551+
const { loadingSpinnerShown } = this.state;
2552+
if (model.isViewportPending && !loadingSpinnerShown) {
2553+
// We only want to show the viewport loading if the viewport is still loading
2554+
// and we're not already showing a loader for something else
2555+
this.startLoading('Waiting for viewport...', {
2556+
loadingCancelShown: false,
2557+
loadingBlocksGrid: false,
2558+
});
2559+
}
2560+
},
2561+
VIEWPORT_LOADING_DELAY,
2562+
{ leading: false, trailing: true }
2563+
);
2564+
25072565
showAllColumns(): void {
25082566
const { metricCalculator } = this.state;
25092567
const userColumnWidths = metricCalculator.getUserColumnWidths();
@@ -2891,7 +2949,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
28912949
}
28922950

28932951
handleFilterBarChange(value: string): void {
2894-
this.startLoading('Filtering...', true);
2952+
this.startLoading('Filtering...', { resetRanges: true });
28952953

28962954
this.setState(({ focusedFilterBarColumn, quickFilters }) => {
28972955
const newQuickFilters = new Map(quickFilters);
@@ -2979,12 +3037,12 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
29793037
const { partitionConfig } = this.state;
29803038
if (isMissingPartitionError(error) && partitionConfig != null) {
29813039
// We'll try loading the initial partition again
2982-
this.startLoading('Reloading partition...', true);
3040+
this.startLoading('Reloading partition...', { resetRanges: true });
29833041
this.setState({ partitionConfig: undefined }, () => {
29843042
this.initState();
29853043
});
29863044
} else if (this.canRollback()) {
2987-
this.startLoading('Rolling back changes...', true);
3045+
this.startLoading('Rolling back changes...', { resetRanges: true });
29883046
this.rollback();
29893047
} else {
29903048
log.error('Table failed and unable to rollback');
@@ -4089,6 +4147,8 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
40894147
loadingText,
40904148
loadingScrimProgress,
40914149
loadingSpinnerShown,
4150+
loadingCancelShown,
4151+
loadingBlocksGrid,
40924152
shownColumnTooltip,
40934153
hoverAdvancedFilter,
40944154
shownAdvancedFilter,
@@ -4261,7 +4321,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
42614321
type="button"
42624322
onClick={this.handleCancel}
42634323
className={classNames('iris-grid-btn-cancel', {
4264-
show: loadingSpinnerShown,
4324+
show: loadingCancelShown,
42654325
})}
42664326
>
42674327
<FontAwesomeIcon icon={vsClose} transform="down-1" />
@@ -4271,7 +4331,10 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
42714331
);
42724332
const gridY = metrics ? metrics.gridY : 0;
42734333
loadingElement = (
4274-
<div className="iris-grid-loading" style={{ top: gridY }}>
4334+
<div
4335+
className="iris-grid-loading"
4336+
style={loadingBlocksGrid ? { top: gridY } : {}}
4337+
>
42754338
{loadingStatus}
42764339
</div>
42774340
);

packages/iris-grid/src/IrisGridModel.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ abstract class IrisGridModel<
7474
DISCONNECT: 'DISCONNECT',
7575
RECONNECT: 'RECONNECT',
7676
TOTALS_UPDATED: 'TOTALS_UPDATED',
77+
/** Fired when the viewport is applied to the table and we're waiting for a response. */
7778
PENDING_DATA_UPDATED: 'PENDING_DATA_UPDATED',
79+
VIEWPORT_UPDATED: 'VIEWPORT_UPDATED',
7880
} as const);
7981

8082
constructor(dh: typeof DhType) {
@@ -484,6 +486,13 @@ abstract class IrisGridModel<
484486
*/
485487
abstract commitPending(): Promise<void>;
486488

489+
/**
490+
* Check if viewport is still loading data
491+
*/
492+
get isViewportPending(): boolean {
493+
return false;
494+
}
495+
487496
/**
488497
* Check if a column is filterable
489498
* @param columnIndex The column index to check for filterability

packages/iris-grid/src/IrisGridProxyModel.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,10 @@ class IrisGridProxyModel extends IrisGridModel implements PartitionedGridModel {
697697
return isEditableGridModel(this.model) && this.model.isEditable;
698698
}
699699

700+
get isViewportPending(): boolean {
701+
return this.model.isViewportPending;
702+
}
703+
700704
isEditableRange: IrisGridTableModel['isEditableRange'] = (
701705
...args
702706
): boolean => {

packages/iris-grid/src/IrisGridTableModelTemplate.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,41 @@ class IrisGridTableModelTemplate<
455455
return !this.isSaveInProgress && this.inputTable != null;
456456
}
457457

458+
get isViewportPending(): boolean {
459+
if (
460+
this.viewport == null ||
461+
this.viewport.columns === undefined ||
462+
this.viewportData == null
463+
) {
464+
return true;
465+
}
466+
// no columns or no rows
467+
if (
468+
this.viewport.columns.length === 0 ||
469+
this.viewportData.rows.length === 0
470+
) {
471+
return false;
472+
}
473+
474+
// offset is first row of loaded data
475+
const pendingTop = this.viewport.top < this.viewportData.offset;
476+
// offset + row.length is last row of loaded data
477+
const pendingBottom =
478+
this.viewportData.offset + this.viewportData.rows.length <
479+
this.viewport.bottom;
480+
// left column doesn't exist in data
481+
const pendingLeft =
482+
this.viewportData.rows[0].data.get(this.viewport.columns[0].index) ===
483+
undefined;
484+
// right column doesn't exist in data
485+
const pendingRight =
486+
this.viewportData.rows[0].data.get(
487+
this.viewport.columns[this.viewport.columns.length - 1].index
488+
) === undefined;
489+
490+
return pendingTop || pendingBottom || pendingLeft || pendingRight;
491+
}
492+
458493
cacheFormattedValue(x: ModelIndex, y: ModelIndex, text: string | null): void {
459494
if (this.formattedStringData[x] == null) {
460495
this.formattedStringData[x] = [];
@@ -1311,6 +1346,9 @@ class IrisGridTableModelTemplate<
13111346
viewportBottom: number,
13121347
columns?: DhType.Column[]
13131348
): void {
1349+
this.dispatchEvent(
1350+
new EventShimCustomEvent(IrisGridModel.EVENT.VIEWPORT_UPDATED)
1351+
);
13141352
log.debug2('applyBufferedViewport', viewportTop, viewportBottom, columns);
13151353
if (this.subscription == null) {
13161354
log.debug2('applyBufferedViewport creating new subscription');

0 commit comments

Comments
 (0)