diff --git a/changelogs/fragments/6683.yml b/changelogs/fragments/6683.yml new file mode 100644 index 000000000000..1a065c9c0c51 --- /dev/null +++ b/changelogs/fragments/6683.yml @@ -0,0 +1,2 @@ +feat: +- Optimize scrolling behavior of Discover table ([#6683](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6683)) diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx index cdd6cf8681ac..88eaae9daa47 100644 --- a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx @@ -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'; diff --git a/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx b/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx index fb9d03604418..a66a9044873b 100644 --- a/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx +++ b/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx @@ -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, @@ -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(null); - const [sentinelEle, setSentinelEle] = useState(); - // 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(); + // `tableElement` is used for first auto-sizing and then fixing column widths + const [tableElement, setTableElement] = useState(); + // 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, @@ -124,6 +151,60 @@ const DefaultDiscoverTableUI = ({ setActivePage(pageNumber); }; + // Lazy-loader of rows + const lazyLoadRequestFrameRef = useRef(0); + const lazyLoadLastTimeRef = useRef(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(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 && ( <> @@ -138,7 +219,7 @@ const DefaultDiscoverTableUI = ({ sampleSize={sampleSize} /> ) : null} - +
{ return ( -
+ {displayedColumns.map((col) => { return ( ); }; + +export const TableRow = React.memo(TableRowUI); diff --git a/test/functional/apps/dashboard/dashboard_time_picker.js b/test/functional/apps/dashboard/dashboard_time_picker.js index bc2add60a33f..2192816f2db7 100644 --- a/test/functional/apps/dashboard/dashboard_time_picker.js +++ b/test/functional/apps/dashboard/dashboard_time_picker.js @@ -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( diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 20419a4730f5..fb2f6115efbf 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -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');