diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 5143293d345..ceeb7772aec 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -938,6 +938,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { rows={logRows} scrollElement={logsContainerRef.current} sortOrder={logsSortOrder} + app={CoreApp.Explore} > { return { navContainer: css` max-height: ${navContainerHeight}; + ${oldestLogsFirst ? 'width: 58px;' : ''} display: flex; flex-direction: column; ${config.featureToggles.logsInfiniteScrolling diff --git a/public/app/features/logs/components/InfiniteScroll.test.tsx b/public/app/features/logs/components/InfiniteScroll.test.tsx index e90e93eb45b..e5e7854dd72 100644 --- a/public/app/features/logs/components/InfiniteScroll.test.tsx +++ b/public/app/features/logs/components/InfiniteScroll.test.tsx @@ -1,7 +1,8 @@ import { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { useEffect, useRef, useState } from 'react'; -import { LogRowModel, dateTimeForTimeZone } from '@grafana/data'; +import { CoreApp, LogRowModel, dateTimeForTimeZone } from '@grafana/data'; import { convertRawToRange } from '@grafana/data/src/datetime/rangeutil'; import { config } from '@grafana/runtime'; import { LogsSortOrder } from '@grafana/schema'; @@ -51,7 +52,13 @@ function ScrollWithWrapper({ children, ...props }: Props) { ); } -function setup(loadMoreMock: () => void, startPosition: number, rows: LogRowModel[], order: LogsSortOrder) { +function setup( + loadMoreMock: () => void, + startPosition: number, + rows: LogRowModel[], + order: LogsSortOrder, + app?: CoreApp +) { const { element, events } = getMockElement(startPosition); function scrollTo(position: number) { @@ -84,6 +91,7 @@ function setup(loadMoreMock: () => void, startPosition: number, rows: LogRowMode scrollElement={element as unknown as HTMLDivElement} loadMoreLogs={loadMoreMock} topScrollEnabled + app={app} >
@@ -267,6 +275,28 @@ describe('InfiniteScroll', () => { }); } ); + + describe('In Explore', () => { + test('Requests older logs from the oldest timestamp', async () => { + const loadMoreMock = jest.fn(); + const rows = createLogRows( + absoluteRange.from + 2 * SCROLLING_THRESHOLD, + absoluteRange.to - 2 * SCROLLING_THRESHOLD + ); + setup(loadMoreMock, 0, rows, LogsSortOrder.Ascending, CoreApp.Explore); + + expect(await screen.findByTestId('contents')).toBeInTheDocument(); + + await screen.findByText('Older logs'); + + await userEvent.click(screen.getByText('Older logs')); + + expect(loadMoreMock).toHaveBeenCalledWith({ + from: absoluteRange.from, + to: rows[0].timeEpochMs, + }); + }); + }); }); function createLogRows(from: number, to: number) { @@ -292,6 +322,7 @@ function getMockElement(scrollTop: number) { clientHeight: 40, scrollTop, scrollTo: jest.fn(), + scroll: jest.fn(), }; return { element, events }; diff --git a/public/app/features/logs/components/InfiniteScroll.tsx b/public/app/features/logs/components/InfiniteScroll.tsx index 65037b4a8b5..9572f0ca283 100644 --- a/public/app/features/logs/components/InfiniteScroll.tsx +++ b/public/app/features/logs/components/InfiniteScroll.tsx @@ -1,14 +1,17 @@ import { css } from '@emotion/css'; -import { ReactNode, useEffect, useRef, useState } from 'react'; +import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; -import { AbsoluteTimeRange, LogRowModel, TimeRange } from '@grafana/data'; +import { AbsoluteTimeRange, CoreApp, LogRowModel, TimeRange } from '@grafana/data'; import { convertRawToRange, isRelativeTime, isRelativeTimeRange } from '@grafana/data/src/datetime/rangeutil'; import { config, reportInteraction } from '@grafana/runtime'; import { LogsSortOrder, TimeZone } from '@grafana/schema'; +import { Button, Icon } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; import { LoadingIndicator } from './LoadingIndicator'; export type Props = { + app?: CoreApp; children: ReactNode; loading: boolean; loadMoreLogs?: (range: AbsoluteTimeRange) => void; @@ -21,6 +24,7 @@ export type Props = { }; export const InfiniteScroll = ({ + app, children, loading, loadMoreLogs, @@ -135,10 +139,37 @@ export const InfiniteScroll = ({ const hideTopMessage = sortOrder === LogsSortOrder.Descending && isRelativeTime(range.raw.to); const hideBottomMessage = sortOrder === LogsSortOrder.Ascending && isRelativeTime(range.raw.to); + const loadOlderLogs = useCallback(() => { + //If we are not on the last page, use next page's range + reportInteraction('grafana_explore_logs_infinite_pagination_clicked', { + pageType: 'olderLogsButton', + }); + const newRange = canScrollTop(getVisibleRange(rows), range, timeZone, sortOrder); + if (!newRange) { + setUpperOutOfRange(true); + return; + } + setUpperOutOfRange(false); + loadMoreLogs?.(newRange); + setUpperLoading(true); + scrollElement?.scroll({ + behavior: 'auto', + top: 0, + }); + }, [loadMoreLogs, range, rows, scrollElement, sortOrder, timeZone]); + return ( <> {upperLoading && } {!hideTopMessage && upperOutOfRange && outOfRangeMessage} + {sortOrder === LogsSortOrder.Ascending && app === CoreApp.Explore && ( + + )} {children} {!hideBottomMessage && lowerOutOfRange && outOfRangeMessage} {lowerLoading && } @@ -151,6 +182,26 @@ const styles = { textAlign: 'center', padding: 0.25, }), + navButton: css({ + width: '58px', + height: '68px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + lineHeight: '1', + position: 'absolute', + top: 0, + right: -3, + zIndex: 1, + }), + navButtonContent: css({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + whiteSpace: 'normal', + }), }; const outOfRangeMessage = ( diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e813d8380fe..8c7211f6943 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1056,6 +1056,11 @@ "new-to-question": "New to Grafana?" } }, + "logs": { + "infinite-scroll": { + "older-logs": "Older logs" + } + }, "migrate-to-cloud": { "build-snapshot": { "description": "This tool can migrate some resources from this installation to your cloud stack. To get started, you'll need to create a snapshot of this installation. Creating a snapshot typically takes less than two minutes. The snapshot is stored alongside this Grafana installation.", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index f8591176bf9..7420889142b 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1056,6 +1056,11 @@ "new-to-question": "Ńęŵ ŧő Ğřäƒäʼnä?" } }, + "logs": { + "infinite-scroll": { + "older-logs": "Øľđęř ľőģş" + } + }, "migrate-to-cloud": { "build-snapshot": { "description": "Ŧĥįş ŧőőľ čäʼn mįģřäŧę şőmę řęşőūřčęş ƒřőm ŧĥįş įʼnşŧäľľäŧįőʼn ŧő yőūř čľőūđ şŧäčĸ. Ŧő ģęŧ şŧäřŧęđ, yőū'ľľ ʼnęęđ ŧő čřęäŧę ä şʼnäpşĥőŧ őƒ ŧĥįş įʼnşŧäľľäŧįőʼn. Cřęäŧįʼnģ ä şʼnäpşĥőŧ ŧypįčäľľy ŧäĸęş ľęşş ŧĥäʼn ŧŵő mįʼnūŧęş. Ŧĥę şʼnäpşĥőŧ įş şŧőřęđ äľőʼnģşįđę ŧĥįş Ğřäƒäʼnä įʼnşŧäľľäŧįőʼn.",