Skip to content

Commit

Permalink
Optimize scrolling behavior of Discover table (#6683)
Browse files Browse the repository at this point in the history
Also:
* some minor cleanup

Signed-off-by: Miki <miki@amazon.com>
(cherry picked from commit 9ac5203)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
github-actions[bot] committed May 1, 2024
1 parent 13b6d42 commit acf48c2
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 27 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/6683.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Optimize scrolling behavior of Discover table ([#6683](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6683))
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
* SPDX-License-Identifier: Apache-2.0
*/

import './_data_grid_table.scss';

import React, { useState } from 'react';
import { EuiPanel } from '@elastic/eui';
import { IndexPattern, getServices } from '../../../opensearch_dashboards_services';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export interface DefaultDiscoverTableProps {
scrollToTop?: () => void;
}

// ToDo: These would need to be read from an upcoming config panel
const PAGINATED_PAGE_SIZE = 50;
const INFINITE_SCROLLED_PAGE_SIZE = 10;

const DefaultDiscoverTableUI = ({
columns,
hits,
Expand Down Expand Up @@ -70,52 +74,75 @@ const DefaultDiscoverTableUI = ({
isShortDots
);
const displayedColumnNames = displayedColumns.map((column) => column.name);
const pageSize = 10;
const [renderedRowCount, setRenderedRowCount] = useState(pageSize); // Start with 10 rows
const [displayedRows, setDisplayedRows] = useState(rows.slice(0, pageSize));

/* INFINITE_SCROLLED_PAGE_SIZE:
* Infinitely scrolling, a page of 10 rows is shown and then 4 pages are lazy-loaded for a total of 5 pages.
* * The lazy-loading is mindful of the performance by monitoring the fps of the browser.
* *`renderedRowCount` and `desiredRowCount` are only used in this method.
*
* PAGINATED_PAGE_SIZE
* Paginated, the view is broken into pages of 50 rows.
* * `displayedRows` and `currentRowCounts` are only used in this method.
*/
const [renderedRowCount, setRenderedRowCount] = useState(INFINITE_SCROLLED_PAGE_SIZE);
const [desiredRowCount, setDesiredRowCount] = useState(
Math.min(rows.length, 5 * INFINITE_SCROLLED_PAGE_SIZE)
);
const [displayedRows, setDisplayedRows] = useState(rows.slice(0, PAGINATED_PAGE_SIZE));
const [currentRowCounts, setCurrentRowCounts] = useState({
startRow: 0,
endRow: rows.length < pageSize ? rows.length : pageSize,
endRow: rows.length < PAGINATED_PAGE_SIZE ? rows.length : PAGINATED_PAGE_SIZE,
});

const observerRef = useRef<IntersectionObserver | null>(null);
const [sentinelEle, setSentinelEle] = useState<HTMLDivElement>();
// Need a callback ref since the element isn't set on the first render.
// `sentinelElement` is attached to the bottom of the table to observe when the table is scrolled all the way.
const [sentinelElement, setSentinelElement] = useState<HTMLDivElement>();
// `tableElement` is used for first auto-sizing and then fixing column widths
const [tableElement, setTableElement] = useState<HTMLTableElement>();
// Both need callback refs since the elements aren't set on the first render.
const sentinelRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setSentinelEle(node);
setSentinelElement(node);
}
}, []);
const tableRef = useCallback((el: HTMLTableElement | null) => {
if (el !== null) {
setTableElement(el);
}
}, []);

useEffect(() => {
if (sentinelEle) {
if (sentinelElement && !showPagination) {
observerRef.current = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setRenderedRowCount((prevRowCount) => prevRowCount + pageSize); // Load 50 more rows
// Load another batch of rows, some immediately and some lazily
setRenderedRowCount((prevRowCount) => prevRowCount + INFINITE_SCROLLED_PAGE_SIZE);
setDesiredRowCount((prevRowCount) => prevRowCount + 5 * INFINITE_SCROLLED_PAGE_SIZE);
}
},
{ threshold: 1.0 }
);

observerRef.current.observe(sentinelEle);
observerRef.current.observe(sentinelElement);
}

return () => {
if (observerRef.current && sentinelEle) {
observerRef.current.unobserve(sentinelEle);
if (observerRef.current && sentinelElement) {
observerRef.current.unobserve(sentinelElement);
}
};
}, [sentinelEle]);
}, [sentinelElement, showPagination]);

// Page management when using a paginated table
const [activePage, setActivePage] = useState(0);
const pageCount = Math.ceil(rows.length / pageSize);

const pageCount = Math.ceil(rows.length / PAGINATED_PAGE_SIZE);
const goToPage = (pageNumber: number) => {
const startRow = pageNumber * pageSize;
const startRow = pageNumber * PAGINATED_PAGE_SIZE;
const endRow =
rows.length < pageNumber * pageSize + pageSize
rows.length < pageNumber * PAGINATED_PAGE_SIZE + PAGINATED_PAGE_SIZE
? rows.length
: pageNumber * pageSize + pageSize;
: pageNumber * PAGINATED_PAGE_SIZE + PAGINATED_PAGE_SIZE;
setCurrentRowCounts({
startRow,
endRow,
Expand All @@ -124,6 +151,60 @@ const DefaultDiscoverTableUI = ({
setActivePage(pageNumber);
};

// Lazy-loader of rows
const lazyLoadRequestFrameRef = useRef<number>(0);
const lazyLoadLastTimeRef = useRef<number>(0);

React.useEffect(() => {
if (!showPagination) {
const loadMoreRows = (time: number) => {
if (renderedRowCount < desiredRowCount) {
// Load more rows only if fps > 30, when calls are less than 33ms apart
if (time - lazyLoadLastTimeRef.current < 33) {
setRenderedRowCount((prevRowCount) => prevRowCount + INFINITE_SCROLLED_PAGE_SIZE);
}
lazyLoadLastTimeRef.current = time;
lazyLoadRequestFrameRef.current = requestAnimationFrame(loadMoreRows);
}
};
lazyLoadRequestFrameRef.current = requestAnimationFrame(loadMoreRows);
}

return () => cancelAnimationFrame(lazyLoadRequestFrameRef.current);
}, [showPagination, renderedRowCount, desiredRowCount]);

// Allow auto column-sizing using the initially rendered rows and then convert to fixed
const tableLayoutRequestFrameRef = useRef<number>(0);

useEffect(() => {
if (tableElement) {
// Load the first batch of rows and adjust the columns to the contents
tableElement.style.tableLayout = 'auto';

tableLayoutRequestFrameRef.current = requestAnimationFrame(() => {
if (tableElement) {
/* Get the widths for each header cell which is the column's width automatically adjusted to the content of
* the column. Apply the width as a style and change the layout to fixed. This is to
* 1) prevent columns from changing size when more rows are added, and
* 2) speed of rendering time of subsequently added rows.
*
* First cell is skipped because it has a dimention set already, and the last cell is skipped to allow it to
* grow as much as the table needs.
*/
tableElement
.querySelectorAll('thead > tr > th:not(:first-child):not(:last-child)')
.forEach((th) => {
(th as HTMLTableCellElement).style.width = th.getBoundingClientRect().width + 'px';
});

tableElement.style.tableLayout = 'fixed';
}
});
}

return () => cancelAnimationFrame(tableLayoutRequestFrameRef.current);
}, [columns, tableElement]);

return (
indexPattern && (
<>
Expand All @@ -138,7 +219,7 @@ const DefaultDiscoverTableUI = ({
sampleSize={sampleSize}
/>
) : null}
<table data-test-subj="docTable" className="osd-table table">
<table data-test-subj="docTable" className="osd-table table" ref={tableRef}>
<thead>
<TableHeader
displayedColumns={displayedColumns}
Expand All @@ -155,7 +236,7 @@ const DefaultDiscoverTableUI = ({
(row: OpenSearchSearchHit, index: number) => {
return (
<TableRow
key={index}
key={row._id}
row={row}
columns={displayedColumnNames}
indexPattern={indexPattern}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function TableHeader({
}: Props) {
return (
<tr data-test-subj="docTableHeader" className="osdDocTableHeader">
<th style={{ width: '24px' }} />
<th style={{ width: '28px' }} />
{displayedColumns.map((col) => {
return (
<TableHeaderColumn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface TableRowProps {
isShortDots: boolean;
}

export const TableRow = ({
const TableRowUI = ({
row,
columns,
indexPattern,
Expand Down Expand Up @@ -185,3 +185,5 @@ export const TableRow = ({
</>
);
};

export const TableRow = React.memo(TableRowUI);
4 changes: 2 additions & 2 deletions test/functional/apps/dashboard/dashboard_time_picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export default function ({ getService, getPageObjects }) {
name: 'saved search',
fields: ['bytes', 'agent'],
});
// DefaultDiscoverTable loads 10 rows initially
await dashboardExpect.rowCountFromDefaultDiscoverTable(10);
// DefaultDiscoverTable loads 10 rows initially and 40 lazily for a total of 50
await dashboardExpect.rowCountFromDefaultDiscoverTable(50);

// Set to time range with no data
await PageObjects.timePicker.setAbsoluteRange(
Expand Down
3 changes: 2 additions & 1 deletion test/functional/apps/home/_sample_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug('Checking area, bar and heatmap charts rendered');
await dashboardExpect.seriesElementCount(15);
log.debug('Checking saved searches rendered');
await dashboardExpect.rowCountFromDefaultDiscoverTable(10);
// DefaultDiscoverTable loads 10 rows initially and 40 lazily for a total of 50
await dashboardExpect.rowCountFromDefaultDiscoverTable(50);
log.debug('Checking input controls rendered');
await dashboardExpect.inputControlItemCount(3);
log.debug('Checking tag cloud rendered');
Expand Down

0 comments on commit acf48c2

Please sign in to comment.