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

Fix row rendering in infinite scroll #8060

Merged
merged 14 commits into from
Sep 10, 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/8060.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix:
- Fix row rendering in Discover infinite scroll ([#8060](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8060))
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { IntlProvider } from 'react-intl';
import { act, render, waitFor, screen } from '@testing-library/react';
import { DefaultDiscoverTable } from './default_discover_table';
import { OpenSearchSearchHit } from '../../doc_views/doc_views_types';
import { coreMock } from '../../../../../../core/public/mocks';
import { getStubIndexPattern } from '../../../../../data/public/test_utils';

jest.mock('../../../opensearch_dashboards_services', () => ({
getServices: jest.fn().mockReturnValue({
uiSettings: {
get: jest.fn().mockImplementation((key) => {
switch (key) {
case 'discover:sampleSize':
return 100;
case 'shortDots:enable':
return true;
case 'doc_table:hideTimeColumn':
return false;
case 'discover:sort:defaultOrder':
return 'desc';
default:
return null;
}
}),
},
}),
}));

describe('DefaultDiscoverTable', () => {
const indexPattern = getStubIndexPattern(
'test-index-pattern',
(cfg) => cfg,
'@timestamp',
[
{ name: 'textField', type: 'text' },
{ name: 'longField', type: 'long' },
{ name: '@timestamp', type: 'date' },
],
coreMock.createSetup()
);

// Generate 50 hits with sample fields
const hits = [...Array(100).keys()].map((key) => {
return {
_id: key.toString(),
fields: {
textField: `value${key}`,
longField: key,
'@timestamp': new Date((1720000000 + key) * 1000),
},
};
});

const getDefaultDiscoverTable = (hitsOverride?: OpenSearchSearchHit[]) => (
<IntlProvider locale="en">
<DefaultDiscoverTable
columns={['textField', 'longField', '@timestamp']}
rows={(hitsOverride ?? hits) as OpenSearchSearchHit[]}
indexPattern={indexPattern}
sort={[]}
onSort={jest.fn()}
onRemoveColumn={jest.fn()}
onMoveColumn={jest.fn()}
onAddColumn={jest.fn()}
onFilter={jest.fn()}
/>
</IntlProvider>
);

let intersectionObserverCallback: (entries: IntersectionObserverEntry[]) => void = (_) => {};
const mockIntersectionObserver = jest.fn();

beforeEach(() => {
mockIntersectionObserver.mockImplementation((...args) => {
intersectionObserverCallback = args[0];
return {
observe: () => null,
unobserve: () => null,
disconnect: () => null,
};
});
window.IntersectionObserver = mockIntersectionObserver;
});

it('should render the correct number of rows initially', () => {
const { container } = render(getDefaultDiscoverTable());

const tableRows = container.querySelectorAll('tbody tr');
expect(tableRows.length).toBe(10);
});

it('should load more rows when scrolling to the bottom', async () => {
const { container } = render(getDefaultDiscoverTable());

const sentinel = container.querySelector('div[data-test-subj="discoverRenderedRowsProgress"]');
const mockScrollEntry = { isIntersecting: true, target: sentinel };
act(() => {
intersectionObserverCallback([mockScrollEntry] as IntersectionObserverEntry[]);
});

await waitFor(() => {
const tableRows = container.querySelectorAll('tbody tr');
expect(tableRows.length).toBe(20);
});
});

it('should display the sample size callout when all rows are rendered', async () => {
const { container } = render(getDefaultDiscoverTable());

let sentinel = container.querySelector('div[data-test-subj="discoverRenderedRowsProgress"]');

// Simulate scrolling to the bottom until all rows are rendered
while (sentinel) {
const mockScrollEntry = { isIntersecting: true, target: sentinel };
act(() => {
intersectionObserverCallback([mockScrollEntry] as IntersectionObserverEntry[]);
});
sentinel = container.querySelector('div[data-test-subj="discoverRenderedRowsProgress"]');
}

await waitFor(() => {
const callout = screen.getByTestId('discoverDocTableFooter');
expect(callout).toBeInTheDocument();
});
});

it('Should restart rendering when new data is available', async () => {
const truncHits = hits.slice(0, 35) as OpenSearchSearchHit[];
const { container, rerender } = render(getDefaultDiscoverTable(truncHits));

let sentinel = container.querySelector('div[data-test-subj="discoverRenderedRowsProgress"]');

// Keep scrolling until all the current rows are exhausted
while (sentinel) {
const mockScrollEntry = { isIntersecting: true, target: sentinel };
act(() => {
intersectionObserverCallback([mockScrollEntry] as IntersectionObserverEntry[]);
});
sentinel = container.querySelector('div[data-test-subj="discoverRenderedRowsProgress"]');
}

// Make the other rows available
rerender(getDefaultDiscoverTable(hits as OpenSearchSearchHit[]));

await waitFor(() => {
const progressSentinel = container.querySelector(
'div[data-test-subj="discoverRenderedRowsProgress"]'
);
expect(progressSentinel).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
// ToDo: These would need to be read from an upcoming config panel
const PAGINATED_PAGE_SIZE = 50;
const INFINITE_SCROLLED_PAGE_SIZE = 10;
// How far to queue unrendered rows ahead of time during infinite scrolling
const DESIRED_ROWS_LOOKAHEAD = 5 * INFINITE_SCROLLED_PAGE_SIZE;

const DefaultDiscoverTableUI = ({
columns,
Expand Down Expand Up @@ -86,7 +88,7 @@
*/
const [renderedRowCount, setRenderedRowCount] = useState(INFINITE_SCROLLED_PAGE_SIZE);
const [desiredRowCount, setDesiredRowCount] = useState(
Math.min(rows.length, 5 * INFINITE_SCROLLED_PAGE_SIZE)
Math.min(rows.length, DESIRED_ROWS_LOOKAHEAD)
);
const [displayedRows, setDisplayedRows] = useState(rows.slice(0, PAGINATED_PAGE_SIZE));
const [currentRowCounts, setCurrentRowCounts] = useState({
Expand Down Expand Up @@ -118,10 +120,14 @@
if (entries[0].isIntersecting) {
// 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);
setDesiredRowCount((prevRowCount) => prevRowCount + DESIRED_ROWS_LOOKAHEAD);

Check warning on line 123 in src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L123 was not covered by tests
}
},
{ threshold: 1.0 }
{
// Important that 0 < threshold < 1, since there OSD application div has a transparent
// fade at the bottom which causes the sentinel element to sometimes not be 100% visible
threshold: 0.1,
}
);

observerRef.current.observe(sentinelElement);
Expand Down Expand Up @@ -155,6 +161,10 @@
const lazyLoadRequestFrameRef = useRef<number>(0);
const lazyLoadLastTimeRef = useRef<number>(0);

// When doing infinite scrolling, the `rows` prop gets regularly updated from the outside: we only
// render the additional rows when we know the load isn't too high. To prevent overloading the
// renderer, we throttle by current framerate and only render if the frames are fast enough, then
// we increase the rendered row count and trigger a re-render.
React.useEffect(() => {
if (!showPagination) {
const loadMoreRows = (time: number) => {
Expand Down Expand Up @@ -254,7 +264,16 @@
</table>
{!showPagination && renderedRowCount < rows.length && (
<div ref={sentinelRef}>
<EuiProgress size="xs" color="accent" />
<EuiProgress
size="xs"
color="accent"
data-test-subj="discoverRenderedRowsProgress"
style={{
// Add a little margin if we aren't rendering the truncation callout below, to make
// the progress bar render better when it's not present
marginBottom: rows.length !== sampleSize ? '5px' : '0',
}}
/>
Comment on lines +267 to +276
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why arent we adding the truncation banner for PPL and SQL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require being able to get the active selected language in-context since PPL and SQL have a different default number of records they return when they have no limit configured, I briefly tried to follow the stack trace to see if I could pass that context down but couldn't find where it exists. Figured fixing the core bug was more important than the bells & whistles for this PR, could look at it for a future PR.

</div>
)}
{!showPagination && rows.length === sampleSize && (
Expand Down
Loading