mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
f852bf684a
commit
40c6f741c0
@ -938,6 +938,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||||||
rows={logRows}
|
rows={logRows}
|
||||||
scrollElement={logsContainerRef.current}
|
scrollElement={logsContainerRef.current}
|
||||||
sortOrder={logsSortOrder}
|
sortOrder={logsSortOrder}
|
||||||
|
app={CoreApp.Explore}
|
||||||
>
|
>
|
||||||
<LogRows
|
<LogRows
|
||||||
pinnedLogs={pinnedLogs}
|
pinnedLogs={pinnedLogs}
|
||||||
@ -1053,6 +1054,7 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
position: 'relative',
|
||||||
}),
|
}),
|
||||||
logsTable: css({
|
logsTable: css({
|
||||||
maxHeight: `${tableHeight}px`,
|
maxHeight: `${tableHeight}px`,
|
||||||
|
@ -214,6 +214,7 @@ const getStyles = (theme: GrafanaTheme2, oldestLogsFirst: boolean) => {
|
|||||||
return {
|
return {
|
||||||
navContainer: css`
|
navContainer: css`
|
||||||
max-height: ${navContainerHeight};
|
max-height: ${navContainerHeight};
|
||||||
|
${oldestLogsFirst ? 'width: 58px;' : ''}
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
${config.featureToggles.logsInfiniteScrolling
|
${config.featureToggles.logsInfiniteScrolling
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { act, render, screen } from '@testing-library/react';
|
import { act, render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
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 { convertRawToRange } from '@grafana/data/src/datetime/rangeutil';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { LogsSortOrder } from '@grafana/schema';
|
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);
|
const { element, events } = getMockElement(startPosition);
|
||||||
|
|
||||||
function scrollTo(position: number) {
|
function scrollTo(position: number) {
|
||||||
@ -84,6 +91,7 @@ function setup(loadMoreMock: () => void, startPosition: number, rows: LogRowMode
|
|||||||
scrollElement={element as unknown as HTMLDivElement}
|
scrollElement={element as unknown as HTMLDivElement}
|
||||||
loadMoreLogs={loadMoreMock}
|
loadMoreLogs={loadMoreMock}
|
||||||
topScrollEnabled
|
topScrollEnabled
|
||||||
|
app={app}
|
||||||
>
|
>
|
||||||
<div data-testid="contents" style={{ height: 100 }} />
|
<div data-testid="contents" style={{ height: 100 }} />
|
||||||
</InfiniteScroll>
|
</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) {
|
function createLogRows(from: number, to: number) {
|
||||||
@ -292,6 +322,7 @@ function getMockElement(scrollTop: number) {
|
|||||||
clientHeight: 40,
|
clientHeight: 40,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
scrollTo: jest.fn(),
|
scrollTo: jest.fn(),
|
||||||
|
scroll: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { element, events };
|
return { element, events };
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { css } from '@emotion/css';
|
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 { convertRawToRange, isRelativeTime, isRelativeTimeRange } from '@grafana/data/src/datetime/rangeutil';
|
||||||
import { config, reportInteraction } from '@grafana/runtime';
|
import { config, reportInteraction } from '@grafana/runtime';
|
||||||
import { LogsSortOrder, TimeZone } from '@grafana/schema';
|
import { LogsSortOrder, TimeZone } from '@grafana/schema';
|
||||||
|
import { Button, Icon } from '@grafana/ui';
|
||||||
|
import { Trans } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { LoadingIndicator } from './LoadingIndicator';
|
import { LoadingIndicator } from './LoadingIndicator';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
app?: CoreApp;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
loadMoreLogs?: (range: AbsoluteTimeRange) => void;
|
loadMoreLogs?: (range: AbsoluteTimeRange) => void;
|
||||||
@ -21,6 +24,7 @@ export type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const InfiniteScroll = ({
|
export const InfiniteScroll = ({
|
||||||
|
app,
|
||||||
children,
|
children,
|
||||||
loading,
|
loading,
|
||||||
loadMoreLogs,
|
loadMoreLogs,
|
||||||
@ -135,10 +139,37 @@ export const InfiniteScroll = ({
|
|||||||
const hideTopMessage = sortOrder === LogsSortOrder.Descending && isRelativeTime(range.raw.to);
|
const hideTopMessage = sortOrder === LogsSortOrder.Descending && isRelativeTime(range.raw.to);
|
||||||
const hideBottomMessage = sortOrder === LogsSortOrder.Ascending && 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{upperLoading && <LoadingIndicator adjective={sortOrder === LogsSortOrder.Descending ? 'newer' : 'older'} />}
|
{upperLoading && <LoadingIndicator adjective={sortOrder === LogsSortOrder.Descending ? 'newer' : 'older'} />}
|
||||||
{!hideTopMessage && upperOutOfRange && outOfRangeMessage}
|
{!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}
|
{children}
|
||||||
{!hideBottomMessage && lowerOutOfRange && outOfRangeMessage}
|
{!hideBottomMessage && lowerOutOfRange && outOfRangeMessage}
|
||||||
{lowerLoading && <LoadingIndicator adjective={sortOrder === LogsSortOrder.Descending ? 'older' : 'newer'} />}
|
{lowerLoading && <LoadingIndicator adjective={sortOrder === LogsSortOrder.Descending ? 'older' : 'newer'} />}
|
||||||
@ -151,6 +182,26 @@ const styles = {
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: 0.25,
|
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 = (
|
const outOfRangeMessage = (
|
||||||
|
@ -1056,6 +1056,11 @@
|
|||||||
"new-to-question": "New to Grafana?"
|
"new-to-question": "New to Grafana?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"logs": {
|
||||||
|
"infinite-scroll": {
|
||||||
|
"older-logs": "Older logs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"migrate-to-cloud": {
|
"migrate-to-cloud": {
|
||||||
"build-snapshot": {
|
"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.",
|
"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.",
|
||||||
|
@ -1056,6 +1056,11 @@
|
|||||||
"new-to-question": "Ńęŵ ŧő Ğřäƒäʼnä?"
|
"new-to-question": "Ńęŵ ŧő Ğřäƒäʼnä?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"logs": {
|
||||||
|
"infinite-scroll": {
|
||||||
|
"older-logs": "Øľđęř ľőģş"
|
||||||
|
}
|
||||||
|
},
|
||||||
"migrate-to-cloud": {
|
"migrate-to-cloud": {
|
||||||
"build-snapshot": {
|
"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.",
|
"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.",
|
||||||
|
Loading…
Reference in New Issue
Block a user