Logs: Show older logs button when infinite scroll is enabled and sort order is descending (#91060)

* LogsNavigation: show older logs button when the order is descending

* LogsNavigation: adjust styles for showing only older logs button

* Logs Navigation: revert changes

* Infinite scroll: add older logs button

* Older logs button: show only in explore

* chore: add unit test

* Formatting

* Update public/app/features/logs/components/InfiniteScroll.test.tsx

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>

* Chore: add missing translation

* Chore: move the button a tiny bit

---------

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>
This commit is contained in:
Matias Chomicki 2024-08-15 15:17:47 +00:00 committed by GitHub
parent f852bf684a
commit 40c6f741c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 99 additions and 4 deletions

View File

@ -938,6 +938,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
rows={logRows}
scrollElement={logsContainerRef.current}
sortOrder={logsSortOrder}
app={CoreApp.Explore}
>
<LogRows
pinnedLogs={pinnedLogs}
@ -1053,6 +1054,7 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
position: 'relative',
}),
logsTable: css({
maxHeight: `${tableHeight}px`,

View File

@ -214,6 +214,7 @@ const getStyles = (theme: GrafanaTheme2, oldestLogsFirst: boolean) => {
return {
navContainer: css`
max-height: ${navContainerHeight};
${oldestLogsFirst ? 'width: 58px;' : ''}
display: flex;
flex-direction: column;
${config.featureToggles.logsInfiniteScrolling

View File

@ -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}
>
<div data-testid="contents" style={{ height: 100 }} />
</InfiniteScroll>
@ -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 };

View File

@ -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 && <LoadingIndicator adjective={sortOrder === LogsSortOrder.Descending ? 'newer' : 'older'} />}
{!hideTopMessage && upperOutOfRange && outOfRangeMessage}
{sortOrder === LogsSortOrder.Ascending && app === CoreApp.Explore && (
<Button className={styles.navButton} variant="secondary" onClick={loadOlderLogs} disabled={loading}>
<div className={styles.navButtonContent}>
<Icon name="angle-up" size="lg" />
<Trans i18nKey="logs.infinite-scroll.older-logs">Older logs</Trans>
</div>
</Button>
)}
{children}
{!hideBottomMessage && lowerOutOfRange && outOfRangeMessage}
{lowerLoading && <LoadingIndicator adjective={sortOrder === LogsSortOrder.Descending ? 'older' : 'newer'} />}
@ -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 = (

View File

@ -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.",

View File

@ -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.",