Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Optimize scrolling behavior of Discover table #6696

Merged
merged 1 commit into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading