The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/logs/components/InfiniteScroll.tsx

229 lines
7.3 KiB

Logs: add infinite scrolling to Explore (#76348) * Explore: propose action, thunk, and decorators for load more * LogsContainer: add loadMore method * Query: remove unused var * Loading more: use navigation to simulate scrolling * Explore: figure out data combination * Fix imports * Explore: deduplicate results when using query splitting * LogsNavigation: add scroll behavior * Remove old code * Scroll: adjust delta value * Load more: remove refIds from signature We can resolve them inside Explore state * Load more: rename to loadMoreLogs * Infinite scrolling: use scrollElement to listen to scrolling events * Explore logs: add fixed height to scrollable logs container * Logs: make logs container the scrolling element * Logs: remove dynamic logs container size It works very well with 1 query, but breaks with more than 1 query or when Logs is not the last rendered panel * Logs navigation: revert changes * Infinite scroll: create component * Infinite scroll: refactor and clean up effect * Infinite scroll: support oldest first scrolling direction * Infinite scroll: support loading oldest logs in ascending and descending order * Infinite scroll: use scroll to top from logs navigation * Logs: make logs container smaller * Logs: make container smaller * State: integrate explore's loading states * Infinite scroll: add loading to effect dependency array * Infinite scroll: display message when scroll limit is reached * Infinite scroll: add support to scroll in both directions * Infinite scroll: capture wheel events for top scroll * scrollableLogsContainer: deprecate in favor of logsInfiniteScrolling * Infinite scroll: implement timerange limits * Infinite scroll: pass timezone * Fix unused variables and imports * Infinite scroll: implement timerange limits for absolute time * Infinite scroll: fix timerange limits for absolute and relative times * Infinite scroll: reset out-of-bounds message * Logs: make container taller * Line limit: use "displayed" instead of "returned" for infinite scrolling * Infinite scrolling: disable behavior when there is no scroll * Remove console log * Infinite scroll: hide limit reached message when using relative time * Logs: migrate styles to object notation * Prettier formatting * LogsModel: fix import order * Update betterer.results * Logs: remove exploreScrollableLogsContainer test * Infinite scroll: display loader * Infinite scroll: improve wheel handling * Explore: unify correlations code * Explore: move new function to helpers * Remove comment * Fix imports * Formatting * Query: add missing awaits in unit test * Logs model: add unit test * Combine frames: move code to feature/logs * Explore: move getCorrelations call back to query It was causing a weird test failure * Fix imports * Infinite scroll: parametrize scrolling threshold * Logs: fix overflow css * Infinite scroll: add basic unit test * Infinite scroll: add unit test for absolute time ranges * Formatting * Explore query: add custom interaction for scrolling * Query: move correlations before update time * Fix import in test * Update comment * Remove comment * Remove comment * Infinite scroll: report interactions from component * Fix import order * Rename action * Infinite scroll: update limit reached message * Explore logs: remove type assertion * Update betterer
1 year ago
import { css } from '@emotion/css';
import React, { ReactNode, useEffect, useState } from 'react';
import { AbsoluteTimeRange, LogRowModel, TimeRange } from '@grafana/data';
import { convertRawToRange, isRelativeTime, isRelativeTimeRange } from '@grafana/data/src/datetime/rangeutil';
import { reportInteraction } from '@grafana/runtime';
import { LogsSortOrder, TimeZone } from '@grafana/schema';
import { Spinner } from '@grafana/ui';
export type Props = {
children: ReactNode;
loading: boolean;
loadMoreLogs?: (range: AbsoluteTimeRange) => void;
range: TimeRange;
rows: LogRowModel[];
scrollElement?: HTMLDivElement;
sortOrder: LogsSortOrder;
timeZone: TimeZone;
};
export const InfiniteScroll = ({
children,
loading,
loadMoreLogs,
range,
rows,
scrollElement,
sortOrder,
timeZone,
}: Props) => {
const [upperOutOfRange, setUpperOutOfRange] = useState(false);
const [lowerOutOfRange, setLowerOutOfRange] = useState(false);
const [upperLoading, setUpperLoading] = useState(false);
const [lowerLoading, setLowerLoading] = useState(false);
const [lastScroll, setLastScroll] = useState(scrollElement?.scrollTop || 0);
useEffect(() => {
setUpperOutOfRange(false);
setLowerOutOfRange(false);
}, [range, rows, sortOrder]);
useEffect(() => {
if (!loading) {
setUpperLoading(false);
setLowerLoading(false);
}
}, [loading]);
useEffect(() => {
if (!scrollElement || !loadMoreLogs) {
return;
}
function handleScroll(event: Event | WheelEvent) {
if (!scrollElement || !loadMoreLogs || !rows.length || loading) {
return;
}
event.stopImmediatePropagation();
setLastScroll(scrollElement.scrollTop);
const scrollDirection = shouldLoadMore(event, scrollElement, lastScroll);
if (scrollDirection === ScrollDirection.NoScroll) {
return;
} else if (scrollDirection === ScrollDirection.Top) {
scrollTop();
} else {
scrollBottom();
}
}
function scrollTop() {
if (!canScrollTop(getVisibleRange(rows), range, timeZone, sortOrder)) {
setUpperOutOfRange(true);
return;
}
setUpperOutOfRange(false);
const newRange =
sortOrder === LogsSortOrder.Descending
? getNextRange(getVisibleRange(rows), range, timeZone)
: getPrevRange(getVisibleRange(rows), range);
loadMoreLogs?.(newRange);
setUpperLoading(true);
reportInteraction('grafana_logs_infinite_scrolling', {
direction: 'top',
sort_order: sortOrder,
});
}
function scrollBottom() {
if (!canScrollBottom(getVisibleRange(rows), range, timeZone, sortOrder)) {
setLowerOutOfRange(true);
return;
}
setLowerOutOfRange(false);
const newRange =
sortOrder === LogsSortOrder.Descending
? getPrevRange(getVisibleRange(rows), range)
: getNextRange(getVisibleRange(rows), range, timeZone);
loadMoreLogs?.(newRange);
setLowerLoading(true);
reportInteraction('grafana_logs_infinite_scrolling', {
direction: 'bottom',
sort_order: sortOrder,
});
}
scrollElement.addEventListener('scroll', handleScroll);
scrollElement.addEventListener('wheel', handleScroll);
return () => {
scrollElement.removeEventListener('scroll', handleScroll);
scrollElement.removeEventListener('wheel', handleScroll);
};
}, [lastScroll, loadMoreLogs, loading, range, rows, scrollElement, sortOrder, timeZone]);
// We allow "now" to move when using relative time, so we hide the message so it doesn't flash.
const hideTopMessage = sortOrder === LogsSortOrder.Descending && isRelativeTime(range.raw.to);
const hideBottomMessage = sortOrder === LogsSortOrder.Ascending && isRelativeTime(range.raw.to);
return (
<>
{upperLoading && loadingMessage}
{!hideTopMessage && upperOutOfRange && outOfRangeMessage}
{children}
{!hideBottomMessage && lowerOutOfRange && outOfRangeMessage}
{lowerLoading && loadingMessage}
</>
);
};
const styles = {
messageContainer: css({
Logs: add infinite scrolling to Explore (#76348) * Explore: propose action, thunk, and decorators for load more * LogsContainer: add loadMore method * Query: remove unused var * Loading more: use navigation to simulate scrolling * Explore: figure out data combination * Fix imports * Explore: deduplicate results when using query splitting * LogsNavigation: add scroll behavior * Remove old code * Scroll: adjust delta value * Load more: remove refIds from signature We can resolve them inside Explore state * Load more: rename to loadMoreLogs * Infinite scrolling: use scrollElement to listen to scrolling events * Explore logs: add fixed height to scrollable logs container * Logs: make logs container the scrolling element * Logs: remove dynamic logs container size It works very well with 1 query, but breaks with more than 1 query or when Logs is not the last rendered panel * Logs navigation: revert changes * Infinite scroll: create component * Infinite scroll: refactor and clean up effect * Infinite scroll: support oldest first scrolling direction * Infinite scroll: support loading oldest logs in ascending and descending order * Infinite scroll: use scroll to top from logs navigation * Logs: make logs container smaller * Logs: make container smaller * State: integrate explore's loading states * Infinite scroll: add loading to effect dependency array * Infinite scroll: display message when scroll limit is reached * Infinite scroll: add support to scroll in both directions * Infinite scroll: capture wheel events for top scroll * scrollableLogsContainer: deprecate in favor of logsInfiniteScrolling * Infinite scroll: implement timerange limits * Infinite scroll: pass timezone * Fix unused variables and imports * Infinite scroll: implement timerange limits for absolute time * Infinite scroll: fix timerange limits for absolute and relative times * Infinite scroll: reset out-of-bounds message * Logs: make container taller * Line limit: use "displayed" instead of "returned" for infinite scrolling * Infinite scrolling: disable behavior when there is no scroll * Remove console log * Infinite scroll: hide limit reached message when using relative time * Logs: migrate styles to object notation * Prettier formatting * LogsModel: fix import order * Update betterer.results * Logs: remove exploreScrollableLogsContainer test * Infinite scroll: display loader * Infinite scroll: improve wheel handling * Explore: unify correlations code * Explore: move new function to helpers * Remove comment * Fix imports * Formatting * Query: add missing awaits in unit test * Logs model: add unit test * Combine frames: move code to feature/logs * Explore: move getCorrelations call back to query It was causing a weird test failure * Fix imports * Infinite scroll: parametrize scrolling threshold * Logs: fix overflow css * Infinite scroll: add basic unit test * Infinite scroll: add unit test for absolute time ranges * Formatting * Explore query: add custom interaction for scrolling * Query: move correlations before update time * Fix import in test * Update comment * Remove comment * Remove comment * Infinite scroll: report interactions from component * Fix import order * Rename action * Infinite scroll: update limit reached message * Explore logs: remove type assertion * Update betterer
1 year ago
textAlign: 'center',
padding: 0.25,
}),
};
const outOfRangeMessage = (
<div className={styles.messageContainer} data-testid="end-of-range">
End of the selected time range.
</div>
);
Logs: add infinite scrolling to Explore (#76348) * Explore: propose action, thunk, and decorators for load more * LogsContainer: add loadMore method * Query: remove unused var * Loading more: use navigation to simulate scrolling * Explore: figure out data combination * Fix imports * Explore: deduplicate results when using query splitting * LogsNavigation: add scroll behavior * Remove old code * Scroll: adjust delta value * Load more: remove refIds from signature We can resolve them inside Explore state * Load more: rename to loadMoreLogs * Infinite scrolling: use scrollElement to listen to scrolling events * Explore logs: add fixed height to scrollable logs container * Logs: make logs container the scrolling element * Logs: remove dynamic logs container size It works very well with 1 query, but breaks with more than 1 query or when Logs is not the last rendered panel * Logs navigation: revert changes * Infinite scroll: create component * Infinite scroll: refactor and clean up effect * Infinite scroll: support oldest first scrolling direction * Infinite scroll: support loading oldest logs in ascending and descending order * Infinite scroll: use scroll to top from logs navigation * Logs: make logs container smaller * Logs: make container smaller * State: integrate explore's loading states * Infinite scroll: add loading to effect dependency array * Infinite scroll: display message when scroll limit is reached * Infinite scroll: add support to scroll in both directions * Infinite scroll: capture wheel events for top scroll * scrollableLogsContainer: deprecate in favor of logsInfiniteScrolling * Infinite scroll: implement timerange limits * Infinite scroll: pass timezone * Fix unused variables and imports * Infinite scroll: implement timerange limits for absolute time * Infinite scroll: fix timerange limits for absolute and relative times * Infinite scroll: reset out-of-bounds message * Logs: make container taller * Line limit: use "displayed" instead of "returned" for infinite scrolling * Infinite scrolling: disable behavior when there is no scroll * Remove console log * Infinite scroll: hide limit reached message when using relative time * Logs: migrate styles to object notation * Prettier formatting * LogsModel: fix import order * Update betterer.results * Logs: remove exploreScrollableLogsContainer test * Infinite scroll: display loader * Infinite scroll: improve wheel handling * Explore: unify correlations code * Explore: move new function to helpers * Remove comment * Fix imports * Formatting * Query: add missing awaits in unit test * Logs model: add unit test * Combine frames: move code to feature/logs * Explore: move getCorrelations call back to query It was causing a weird test failure * Fix imports * Infinite scroll: parametrize scrolling threshold * Logs: fix overflow css * Infinite scroll: add basic unit test * Infinite scroll: add unit test for absolute time ranges * Formatting * Explore query: add custom interaction for scrolling * Query: move correlations before update time * Fix import in test * Update comment * Remove comment * Remove comment * Infinite scroll: report interactions from component * Fix import order * Rename action * Infinite scroll: update limit reached message * Explore logs: remove type assertion * Update betterer
1 year ago
const loadingMessage = (
<div className={styles.messageContainer}>
Logs: add infinite scrolling to Explore (#76348) * Explore: propose action, thunk, and decorators for load more * LogsContainer: add loadMore method * Query: remove unused var * Loading more: use navigation to simulate scrolling * Explore: figure out data combination * Fix imports * Explore: deduplicate results when using query splitting * LogsNavigation: add scroll behavior * Remove old code * Scroll: adjust delta value * Load more: remove refIds from signature We can resolve them inside Explore state * Load more: rename to loadMoreLogs * Infinite scrolling: use scrollElement to listen to scrolling events * Explore logs: add fixed height to scrollable logs container * Logs: make logs container the scrolling element * Logs: remove dynamic logs container size It works very well with 1 query, but breaks with more than 1 query or when Logs is not the last rendered panel * Logs navigation: revert changes * Infinite scroll: create component * Infinite scroll: refactor and clean up effect * Infinite scroll: support oldest first scrolling direction * Infinite scroll: support loading oldest logs in ascending and descending order * Infinite scroll: use scroll to top from logs navigation * Logs: make logs container smaller * Logs: make container smaller * State: integrate explore's loading states * Infinite scroll: add loading to effect dependency array * Infinite scroll: display message when scroll limit is reached * Infinite scroll: add support to scroll in both directions * Infinite scroll: capture wheel events for top scroll * scrollableLogsContainer: deprecate in favor of logsInfiniteScrolling * Infinite scroll: implement timerange limits * Infinite scroll: pass timezone * Fix unused variables and imports * Infinite scroll: implement timerange limits for absolute time * Infinite scroll: fix timerange limits for absolute and relative times * Infinite scroll: reset out-of-bounds message * Logs: make container taller * Line limit: use "displayed" instead of "returned" for infinite scrolling * Infinite scrolling: disable behavior when there is no scroll * Remove console log * Infinite scroll: hide limit reached message when using relative time * Logs: migrate styles to object notation * Prettier formatting * LogsModel: fix import order * Update betterer.results * Logs: remove exploreScrollableLogsContainer test * Infinite scroll: display loader * Infinite scroll: improve wheel handling * Explore: unify correlations code * Explore: move new function to helpers * Remove comment * Fix imports * Formatting * Query: add missing awaits in unit test * Logs model: add unit test * Combine frames: move code to feature/logs * Explore: move getCorrelations call back to query It was causing a weird test failure * Fix imports * Infinite scroll: parametrize scrolling threshold * Logs: fix overflow css * Infinite scroll: add basic unit test * Infinite scroll: add unit test for absolute time ranges * Formatting * Explore query: add custom interaction for scrolling * Query: move correlations before update time * Fix import in test * Update comment * Remove comment * Remove comment * Infinite scroll: report interactions from component * Fix import order * Rename action * Infinite scroll: update limit reached message * Explore logs: remove type assertion * Update betterer
1 year ago
<Spinner />
</div>
);
enum ScrollDirection {
Top = -1,
Bottom = 1,
NoScroll = 0,
}
function shouldLoadMore(event: Event | WheelEvent, element: HTMLDivElement, lastScroll: number): ScrollDirection {
// Disable behavior if there is no scroll
if (element.scrollHeight <= element.clientHeight) {
return ScrollDirection.NoScroll;
}
const delta = event instanceof WheelEvent ? event.deltaY : element.scrollTop - lastScroll;
if (delta === 0) {
return ScrollDirection.NoScroll;
}
const scrollDirection = delta < 0 ? ScrollDirection.Top : ScrollDirection.Bottom;
const diff =
scrollDirection === ScrollDirection.Top
? element.scrollTop
: element.scrollHeight - element.scrollTop - element.clientHeight;
const coef = 1;
return diff <= coef ? scrollDirection : ScrollDirection.NoScroll;
}
function getVisibleRange(rows: LogRowModel[]) {
const firstTimeStamp = rows[0].timeEpochMs;
const lastTimeStamp = rows[rows.length - 1].timeEpochMs;
const visibleRange =
lastTimeStamp < firstTimeStamp
? { from: lastTimeStamp, to: firstTimeStamp }
: { from: firstTimeStamp, to: lastTimeStamp };
return visibleRange;
}
function getPrevRange(visibleRange: AbsoluteTimeRange, currentRange: TimeRange) {
return { from: currentRange.from.valueOf(), to: visibleRange.from };
}
function getNextRange(visibleRange: AbsoluteTimeRange, currentRange: TimeRange, timeZone: TimeZone) {
// When requesting new logs, update the current range if using relative time ranges.
currentRange = updateCurrentRange(currentRange, timeZone);
return { from: visibleRange.to, to: currentRange.to.valueOf() };
}
export const SCROLLING_THRESHOLD = 1e3;
// To get more logs, the difference between the visible range and the current range should be 1 second or more.
function canScrollTop(
visibleRange: AbsoluteTimeRange,
currentRange: TimeRange,
timeZone: TimeZone,
sortOrder: LogsSortOrder
) {
if (sortOrder === LogsSortOrder.Descending) {
// When requesting new logs, update the current range if using relative time ranges.
currentRange = updateCurrentRange(currentRange, timeZone);
return currentRange.to.valueOf() - visibleRange.to > SCROLLING_THRESHOLD;
}
return Math.abs(currentRange.from.valueOf() - visibleRange.from) > SCROLLING_THRESHOLD;
}
function canScrollBottom(
visibleRange: AbsoluteTimeRange,
currentRange: TimeRange,
timeZone: TimeZone,
sortOrder: LogsSortOrder
) {
if (sortOrder === LogsSortOrder.Descending) {
return Math.abs(currentRange.from.valueOf() - visibleRange.from) > SCROLLING_THRESHOLD;
}
// When requesting new logs, update the current range if using relative time ranges.
currentRange = updateCurrentRange(currentRange, timeZone);
return currentRange.to.valueOf() - visibleRange.to > SCROLLING_THRESHOLD;
}
// Given a TimeRange, returns a new instance if using relative time, or else the same.
function updateCurrentRange(timeRange: TimeRange, timeZone: TimeZone) {
return isRelativeTimeRange(timeRange.raw) ? convertRawToRange(timeRange.raw, timeZone) : timeRange;
}