diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LogsViewerPage/LogsViewerPage.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LogsViewerPage/LogsViewerPage.component.test.tsx index 54fde219650..1011a93ddbf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/LogsViewerPage/LogsViewerPage.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/LogsViewerPage/LogsViewerPage.component.test.tsx @@ -36,6 +36,7 @@ import { getExternalApplicationRuns, getLatestApplicationRuns, } from '../../rest/applicationAPI'; +import { getIngestionPipelineLogById } from '../../rest/ingestionPipelineAPI'; import LogsViewerPage from './LogsViewerPage'; const mockScrollToIndex = jest.fn(); @@ -106,24 +107,53 @@ jest.mock('../../hooks/useDownloadProgressStore', () => ({ })), })); -jest.mock('@melloware/react-logviewer', () => ({ - LazyLog: React.forwardRef(({ text }: { text: string }, ref) => { - // Mock the ref structure that the component expects - if (ref) { - (ref as { current: Record }).current = { - state: { - count: 230, - }, - listRef: { - current: { - scrollToIndex: mockScrollToIndex, - }, - }, - }; - } +let mockScrollPosition = { + scrollTop: 80, + scrollHeight: 100, + clientHeight: 20, +}; - return
{text}
; - }), +jest.mock('@melloware/react-logviewer', () => ({ + LazyLog: React.forwardRef( + ( + { + text, + onScroll, + }: { + text: string; + onScroll: (args: { + scrollTop: number; + scrollHeight: number; + clientHeight: number; + }) => void; + }, + ref + ) => { + // Mock the ref structure that the component expects + if (ref) { + (ref as { current: Record }).current = { + state: { + count: 230, + }, + listRef: { + current: { + scrollToIndex: mockScrollToIndex, + }, + }, + }; + } + + return ( +
+ {text} +
onScroll(mockScrollPosition)} + /> +
+ ); + } + ), })); describe('LogsViewerPage.component', () => { @@ -198,4 +228,138 @@ describe('LogsViewerPage.component', () => { // Verify that scrollToIndex was called with the correct parameter (totalLines - 1) expect(mockScrollToIndex).toHaveBeenCalledWith(229); }); + + it('should call handleScroll when user scrolls at the bottom', async () => { + (useParams as jest.Mock).mockReturnValue({ + logEntityType: 'TestSuite', + ingestionName: 'ingestion_123456', + }); + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('scroll-container')).toBeInTheDocument(); + }); + + const scrollContainer = screen.getByTestId('scroll-container'); + + await act(async () => { + fireEvent.click(scrollContainer); + }); + + expect(getIngestionPipelineLogById).toHaveBeenCalledWith( + 'c379d75a-43cd-4d93-a799-0bba4a22c690', + '1' + ); + }); + + it('should not call handleScroll when user scrolls and is not at the bottom', async () => { + // Set scroll position to NOT be at the bottom + mockScrollPosition = { scrollTop: 10, scrollHeight: 100, clientHeight: 20 }; + + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('scroll-container')).toBeInTheDocument(); + }); + + const scrollContainer = screen.getByTestId('scroll-container'); + + // Simulate scroll that is NOT at the bottom + await act(async () => { + fireEvent.click(scrollContainer); + }); + + // Verify that the API was not called since we're not at the bottom + expect(getIngestionPipelineLogById).not.toHaveBeenCalledWith( + 'c379d75a-43cd-4d93-a799-0bba4a22c690', + '1' + ); + }); + + it('should not call handleScroll when user scrolls and is at the bottom with 40- margin', async () => { + mockScrollPosition = { scrollTop: 41, scrollHeight: 100, clientHeight: 20 }; + + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('scroll-container')).toBeInTheDocument(); + }); + + const scrollContainer = screen.getByTestId('scroll-container'); + + // Simulate scroll that is NOT at the bottom + await act(async () => { + fireEvent.click(scrollContainer); + }); + + // Verify that the API was not called since we're not at the bottom + expect(getIngestionPipelineLogById).toHaveBeenCalledWith( + 'c379d75a-43cd-4d93-a799-0bba4a22c690', + '1' + ); + }); + + it('should not call handleScroll when user scrolls and is at the bottom with 40 margin', async () => { + mockScrollPosition = { scrollTop: 40, scrollHeight: 100, clientHeight: 20 }; + + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('scroll-container')).toBeInTheDocument(); + }); + + const scrollContainer = screen.getByTestId('scroll-container'); + + // Simulate scroll that is NOT at the bottom + await act(async () => { + fireEvent.click(scrollContainer); + }); + + // Verify that the API was not called since we're not at the bottom + expect(getIngestionPipelineLogById).not.toHaveBeenCalledWith( + 'c379d75a-43cd-4d93-a799-0bba4a22c690', + '1' + ); + }); + + it('should not call handleScroll when user scrolls and is at the bottom but after is undefined', async () => { + mockScrollPosition = { scrollTop: 80, scrollHeight: 100, clientHeight: 20 }; + (getIngestionPipelineLogById as jest.Mock).mockImplementation(() => + Promise.resolve({ + data: { + ...mockLogsData, + after: undefined, + }, + }) + ); + + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByTestId('scroll-container')).toBeInTheDocument(); + }); + + const scrollContainer = screen.getByTestId('scroll-container'); + + // Simulate scroll that is NOT at the bottom + await act(async () => { + fireEvent.click(scrollContainer); + }); + + // Verify that the API was not called since we're not at the bottom + expect(getIngestionPipelineLogById).not.toHaveBeenCalledWith( + 'c379d75a-43cd-4d93-a799-0bba4a22c690', + '1' + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LogsViewerPage/LogsViewerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LogsViewerPage/LogsViewerPage.tsx index d436cd44a3b..c07318f3e23 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/LogsViewerPage/LogsViewerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/LogsViewerPage/LogsViewerPage.tsx @@ -217,13 +217,17 @@ const LogsViewerPage = () => { } }, []); - const handleScroll = (event: Event) => { - const targetElement = event.target as HTMLDivElement; - - const scrollTop = targetElement.scrollTop; - const scrollHeight = targetElement.scrollHeight; - const clientHeight = targetElement.clientHeight; - const isBottom = clientHeight + scrollTop === scrollHeight; + const handleScroll = (scrollValues: { + scrollTop: number; + scrollHeight: number; + clientHeight: number; + }) => { + const scrollTop = scrollValues.scrollTop; + const scrollHeight = scrollValues.scrollHeight; + const clientHeight = scrollValues.clientHeight; + // Fetch more logs when user is at the bottom of the log + // with a margin of about 40px (approximate height of one line) + const isBottom = Math.abs(clientHeight + scrollTop - scrollHeight) < 40; if ( !isLogsLoading && @@ -238,20 +242,6 @@ const LogsViewerPage = () => { return; }; - useLayoutEffect(() => { - const logBody = document.getElementsByClassName( - 'ReactVirtualized__Grid' - )[0]; - - if (logBody) { - logBody.addEventListener('scroll', handleScroll, { passive: true }); - } - - return () => { - logBody && logBody.removeEventListener('scroll', handleScroll); - }; - }); - useLayoutEffect(() => { const lazyLogSearchBarInput = document.getElementsByClassName( 'react-lazylog-searchbar-input' @@ -420,6 +410,7 @@ const LogsViewerPage = () => { extraLines={1} // 1 is to be add so that linux users can see last line of the log ref={lazyLogRef} text={logs} + onScroll={handleScroll} />