Skip to content

Commit 9ac5203

Browse files
authored
Optimize scrolling behavior of Discover table (#6683)
Also: * some minor cleanup Signed-off-by: Miki <[email protected]>
1 parent 42166d0 commit 9ac5203

File tree

7 files changed

+111
-27
lines changed

7 files changed

+111
-27
lines changed

changelogs/fragments/6683.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Optimize scrolling behavior of Discover table ([#6683](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6683))

src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import './_data_grid_table.scss';
7-
86
import React, { useState } from 'react';
97
import { EuiPanel } from '@elastic/eui';
108
import { IndexPattern, getServices } from '../../../opensearch_dashboards_services';

src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx

+101-20
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export interface DefaultDiscoverTableProps {
3838
scrollToTop?: () => void;
3939
}
4040

41+
// ToDo: These would need to be read from an upcoming config panel
42+
const PAGINATED_PAGE_SIZE = 50;
43+
const INFINITE_SCROLLED_PAGE_SIZE = 10;
44+
4145
const DefaultDiscoverTableUI = ({
4246
columns,
4347
hits,
@@ -70,52 +74,75 @@ const DefaultDiscoverTableUI = ({
7074
isShortDots
7175
);
7276
const displayedColumnNames = displayedColumns.map((column) => column.name);
73-
const pageSize = 10;
74-
const [renderedRowCount, setRenderedRowCount] = useState(pageSize); // Start with 10 rows
75-
const [displayedRows, setDisplayedRows] = useState(rows.slice(0, pageSize));
77+
78+
/* INFINITE_SCROLLED_PAGE_SIZE:
79+
* Infinitely scrolling, a page of 10 rows is shown and then 4 pages are lazy-loaded for a total of 5 pages.
80+
* * The lazy-loading is mindful of the performance by monitoring the fps of the browser.
81+
* *`renderedRowCount` and `desiredRowCount` are only used in this method.
82+
*
83+
* PAGINATED_PAGE_SIZE
84+
* Paginated, the view is broken into pages of 50 rows.
85+
* * `displayedRows` and `currentRowCounts` are only used in this method.
86+
*/
87+
const [renderedRowCount, setRenderedRowCount] = useState(INFINITE_SCROLLED_PAGE_SIZE);
88+
const [desiredRowCount, setDesiredRowCount] = useState(
89+
Math.min(rows.length, 5 * INFINITE_SCROLLED_PAGE_SIZE)
90+
);
91+
const [displayedRows, setDisplayedRows] = useState(rows.slice(0, PAGINATED_PAGE_SIZE));
7692
const [currentRowCounts, setCurrentRowCounts] = useState({
7793
startRow: 0,
78-
endRow: rows.length < pageSize ? rows.length : pageSize,
94+
endRow: rows.length < PAGINATED_PAGE_SIZE ? rows.length : PAGINATED_PAGE_SIZE,
7995
});
96+
8097
const observerRef = useRef<IntersectionObserver | null>(null);
81-
const [sentinelEle, setSentinelEle] = useState<HTMLDivElement>();
82-
// Need a callback ref since the element isn't set on the first render.
98+
// `sentinelElement` is attached to the bottom of the table to observe when the table is scrolled all the way.
99+
const [sentinelElement, setSentinelElement] = useState<HTMLDivElement>();
100+
// `tableElement` is used for first auto-sizing and then fixing column widths
101+
const [tableElement, setTableElement] = useState<HTMLTableElement>();
102+
// Both need callback refs since the elements aren't set on the first render.
83103
const sentinelRef = useCallback((node: HTMLDivElement | null) => {
84104
if (node !== null) {
85-
setSentinelEle(node);
105+
setSentinelElement(node);
106+
}
107+
}, []);
108+
const tableRef = useCallback((el: HTMLTableElement | null) => {
109+
if (el !== null) {
110+
setTableElement(el);
86111
}
87112
}, []);
88113

89114
useEffect(() => {
90-
if (sentinelEle) {
115+
if (sentinelElement && !showPagination) {
91116
observerRef.current = new IntersectionObserver(
92117
(entries) => {
93118
if (entries[0].isIntersecting) {
94-
setRenderedRowCount((prevRowCount) => prevRowCount + pageSize); // Load 50 more rows
119+
// Load another batch of rows, some immediately and some lazily
120+
setRenderedRowCount((prevRowCount) => prevRowCount + INFINITE_SCROLLED_PAGE_SIZE);
121+
setDesiredRowCount((prevRowCount) => prevRowCount + 5 * INFINITE_SCROLLED_PAGE_SIZE);
95122
}
96123
},
97124
{ threshold: 1.0 }
98125
);
99126

100-
observerRef.current.observe(sentinelEle);
127+
observerRef.current.observe(sentinelElement);
101128
}
102129

103130
return () => {
104-
if (observerRef.current && sentinelEle) {
105-
observerRef.current.unobserve(sentinelEle);
131+
if (observerRef.current && sentinelElement) {
132+
observerRef.current.unobserve(sentinelElement);
106133
}
107134
};
108-
}, [sentinelEle]);
135+
}, [sentinelElement, showPagination]);
109136

137+
// Page management when using a paginated table
110138
const [activePage, setActivePage] = useState(0);
111-
const pageCount = Math.ceil(rows.length / pageSize);
112-
139+
const pageCount = Math.ceil(rows.length / PAGINATED_PAGE_SIZE);
113140
const goToPage = (pageNumber: number) => {
114-
const startRow = pageNumber * pageSize;
141+
const startRow = pageNumber * PAGINATED_PAGE_SIZE;
115142
const endRow =
116-
rows.length < pageNumber * pageSize + pageSize
143+
rows.length < pageNumber * PAGINATED_PAGE_SIZE + PAGINATED_PAGE_SIZE
117144
? rows.length
118-
: pageNumber * pageSize + pageSize;
145+
: pageNumber * PAGINATED_PAGE_SIZE + PAGINATED_PAGE_SIZE;
119146
setCurrentRowCounts({
120147
startRow,
121148
endRow,
@@ -124,6 +151,60 @@ const DefaultDiscoverTableUI = ({
124151
setActivePage(pageNumber);
125152
};
126153

154+
// Lazy-loader of rows
155+
const lazyLoadRequestFrameRef = useRef<number>(0);
156+
const lazyLoadLastTimeRef = useRef<number>(0);
157+
158+
React.useEffect(() => {
159+
if (!showPagination) {
160+
const loadMoreRows = (time: number) => {
161+
if (renderedRowCount < desiredRowCount) {
162+
// Load more rows only if fps > 30, when calls are less than 33ms apart
163+
if (time - lazyLoadLastTimeRef.current < 33) {
164+
setRenderedRowCount((prevRowCount) => prevRowCount + INFINITE_SCROLLED_PAGE_SIZE);
165+
}
166+
lazyLoadLastTimeRef.current = time;
167+
lazyLoadRequestFrameRef.current = requestAnimationFrame(loadMoreRows);
168+
}
169+
};
170+
lazyLoadRequestFrameRef.current = requestAnimationFrame(loadMoreRows);
171+
}
172+
173+
return () => cancelAnimationFrame(lazyLoadRequestFrameRef.current);
174+
}, [showPagination, renderedRowCount, desiredRowCount]);
175+
176+
// Allow auto column-sizing using the initially rendered rows and then convert to fixed
177+
const tableLayoutRequestFrameRef = useRef<number>(0);
178+
179+
useEffect(() => {
180+
if (tableElement) {
181+
// Load the first batch of rows and adjust the columns to the contents
182+
tableElement.style.tableLayout = 'auto';
183+
184+
tableLayoutRequestFrameRef.current = requestAnimationFrame(() => {
185+
if (tableElement) {
186+
/* Get the widths for each header cell which is the column's width automatically adjusted to the content of
187+
* the column. Apply the width as a style and change the layout to fixed. This is to
188+
* 1) prevent columns from changing size when more rows are added, and
189+
* 2) speed of rendering time of subsequently added rows.
190+
*
191+
* First cell is skipped because it has a dimention set already, and the last cell is skipped to allow it to
192+
* grow as much as the table needs.
193+
*/
194+
tableElement
195+
.querySelectorAll('thead > tr > th:not(:first-child):not(:last-child)')
196+
.forEach((th) => {
197+
(th as HTMLTableCellElement).style.width = th.getBoundingClientRect().width + 'px';
198+
});
199+
200+
tableElement.style.tableLayout = 'fixed';
201+
}
202+
});
203+
}
204+
205+
return () => cancelAnimationFrame(tableLayoutRequestFrameRef.current);
206+
}, [columns, tableElement]);
207+
127208
return (
128209
indexPattern && (
129210
<>
@@ -138,7 +219,7 @@ const DefaultDiscoverTableUI = ({
138219
sampleSize={sampleSize}
139220
/>
140221
) : null}
141-
<table data-test-subj="docTable" className="osd-table table">
222+
<table data-test-subj="docTable" className="osd-table table" ref={tableRef}>
142223
<thead>
143224
<TableHeader
144225
displayedColumns={displayedColumns}
@@ -155,7 +236,7 @@ const DefaultDiscoverTableUI = ({
155236
(row: OpenSearchSearchHit, index: number) => {
156237
return (
157238
<TableRow
158-
key={index}
239+
key={row._id}
159240
row={row}
160241
columns={displayedColumnNames}
161242
indexPattern={indexPattern}

src/plugins/discover/public/application/components/default_discover_table/table_header.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function TableHeader({
3939
}: Props) {
4040
return (
4141
<tr data-test-subj="docTableHeader" className="osdDocTableHeader">
42-
<th style={{ width: '24px' }} />
42+
<th style={{ width: '28px' }} />
4343
{displayedColumns.map((col) => {
4444
return (
4545
<TableHeaderColumn

src/plugins/discover/public/application/components/default_discover_table/table_row.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface TableRowProps {
3030
isShortDots: boolean;
3131
}
3232

33-
export const TableRow = ({
33+
const TableRowUI = ({
3434
row,
3535
columns,
3636
indexPattern,
@@ -185,3 +185,5 @@ export const TableRow = ({
185185
</>
186186
);
187187
};
188+
189+
export const TableRow = React.memo(TableRowUI);

test/functional/apps/dashboard/dashboard_time_picker.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ export default function ({ getService, getPageObjects }) {
6767
name: 'saved search',
6868
fields: ['bytes', 'agent'],
6969
});
70-
// DefaultDiscoverTable loads 10 rows initially
71-
await dashboardExpect.rowCountFromDefaultDiscoverTable(10);
70+
// DefaultDiscoverTable loads 10 rows initially and 40 lazily for a total of 50
71+
await dashboardExpect.rowCountFromDefaultDiscoverTable(50);
7272

7373
// Set to time range with no data
7474
await PageObjects.timePicker.setAbsoluteRange(

test/functional/apps/home/_sample_data.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
126126
log.debug('Checking area, bar and heatmap charts rendered');
127127
await dashboardExpect.seriesElementCount(15);
128128
log.debug('Checking saved searches rendered');
129-
await dashboardExpect.rowCountFromDefaultDiscoverTable(10);
129+
// DefaultDiscoverTable loads 10 rows initially and 40 lazily for a total of 50
130+
await dashboardExpect.rowCountFromDefaultDiscoverTable(50);
130131
log.debug('Checking input controls rendered');
131132
await dashboardExpect.inputControlItemCount(3);
132133
log.debug('Checking tag cloud rendered');

0 commit comments

Comments
 (0)