grafana/public/app/features/logs/components/panel/InfiniteScroll.tsx

240 lines
7.5 KiB
TypeScript
Raw Normal View History

New Logs Panel: font size selector and Log Details size improvments (#106376) * LogList: create font size option * LogList: prevent option fontSize bouncing * LogListContext: fix stored container size bigger than container * LogList: render smaller font size * virtualization: adjust to variable font size * virtualization: strip white characters of at the start successive long lines * LogList: add font size to log size cache * LogList: use getters instead of fixed constants * LogLine: prevent unnecessary overflow calls * virtualization: strip ansi color codes before measuring * LogListDetails: adjust size on resize and give logs panel a min width * LogsPanel: add showControls as a dashboard option * virtualization: update test * virtualization: add small test case * processing: update font size * LogListControls: update test * Extract translations * Logs Panel: enable controls by default * LogListContext: update mock * ControlledLogRows: add missing prop * LogLine: remove height ref * LogList: dont touch the debounced function on successive calls * LogLine: update test * LogsPanel: make controls default to false again * LogsPanel: make controls default to false again * LogLineDetails: fix height resizing and make close button sticky * LogLine: memo log component * LogLineDetails: fix close button position * New Logs Panel: Add Popover Menu support (#106394) * LogList: add popover menu support * LogList: test popover menu * Chore: remove unnecessary optional chain op * LogLinedDetails: fix close button position with and without scroll
2025-06-10 17:59:01 +08:00
import { ReactNode, useCallback, useEffect, useRef, useState, MouseEvent } from 'react';
import { usePrevious } from 'react-use';
import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window';
import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data';
import { TFunction, useTranslate } from '@grafana/i18n';
import { config, reportInteraction } from '@grafana/runtime';
import { Spinner, useStyles2 } from '@grafana/ui';
import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll';
import { getStyles, LogLine } from './LogLine';
import { LogLineMessage } from './LogLineMessage';
import { LogListModel } from './processing';
interface ChildrenProps {
itemCount: number;
getItemKey: (index: number) => string;
onItemsRendered: (props: ListOnItemsRenderedProps) => void;
Renderer: (props: ListChildComponentProps) => ReactNode;
}
interface Props {
children: (props: ChildrenProps) => ReactNode;
displayedFields: string[];
handleOverflow: (index: number, id: string, height?: number) => void;
loadMore?: (range: AbsoluteTimeRange) => void;
logs: LogListModel[];
New Logs Panel: font size selector and Log Details size improvments (#106376) * LogList: create font size option * LogList: prevent option fontSize bouncing * LogListContext: fix stored container size bigger than container * LogList: render smaller font size * virtualization: adjust to variable font size * virtualization: strip white characters of at the start successive long lines * LogList: add font size to log size cache * LogList: use getters instead of fixed constants * LogLine: prevent unnecessary overflow calls * virtualization: strip ansi color codes before measuring * LogListDetails: adjust size on resize and give logs panel a min width * LogsPanel: add showControls as a dashboard option * virtualization: update test * virtualization: add small test case * processing: update font size * LogListControls: update test * Extract translations * Logs Panel: enable controls by default * LogListContext: update mock * ControlledLogRows: add missing prop * LogLine: remove height ref * LogList: dont touch the debounced function on successive calls * LogLine: update test * LogsPanel: make controls default to false again * LogsPanel: make controls default to false again * LogLineDetails: fix height resizing and make close button sticky * LogLine: memo log component * LogLineDetails: fix close button position * New Logs Panel: Add Popover Menu support (#106394) * LogList: add popover menu support * LogList: test popover menu * Chore: remove unnecessary optional chain op * LogLinedDetails: fix close button position with and without scroll
2025-06-10 17:59:01 +08:00
onClick: (e: MouseEvent<HTMLElement>, log: LogListModel) => void;
scrollElement: HTMLDivElement | null;
setInitialScrollPosition: () => void;
showTime: boolean;
sortOrder: LogsSortOrder;
timeRange: TimeRange;
timeZone: string;
wrapLogMessage: boolean;
}
type InfiniteLoaderState = 'idle' | 'out-of-bounds' | 'pre-scroll' | 'loading';
export const InfiniteScroll = ({
children,
displayedFields,
handleOverflow,
loadMore,
logs,
New Logs Panel: Add Log Details support (#105609) * Log list: add onclick listener * LogListContext: add basic details support * LogLineDetails: create component * Address lint issues * Log Details: make resizable and store size * LogListModel: add sampled and error support * LogDetails: pass more required props * LogLineContext: add interactive callbacks support * LogLineDetails: pass interactive callbacks * LogList: pass displayedFields callbacks * LogLine: move click listener * LogLineMenu: support showing details * LogLine: move onclick listener * LogListContext: remove displayedFields intermediation * i18n * LogListContext: abstract details shown function * LogLine: visually show expanded lines * LogDetails: remove min width for labels * LogLineDetails: add close button * LogList: add extra wrapper to get width * LogLineDetails: update logs size on resize * virtualization: update to new width reference * LogLine: check overflow on every re-render * LogList: debug virtualization when resizing * LogLineDetails: make it scrollable * LogListContext: make detailsWidth not undefined * Update tests with new attributes * LogLine: update collapsed state with container changes * LogLine: move cursor property to clickable styles * LogList: fix height recalculation when display options change * Logs: fix feature toggles support * Logs: more feature toggles adjustments * Lint * LogLine: support duplicates, hasError, and isSampled * Logs: debug feature flag combinations * i18n * Prettier * New Logs Panel: generate storage key for dashboards * Explore Logs: fix filtered levels * Logs Sample: integrate new panel * LogLine: fix unwrapped logs * Fix test * Update test * Logs panel: update test * Prettier * LogLine: update tests * LogLineMenu: update test * LogList: update unit test * processing: update test * virtualization: update unit test
2025-05-21 01:28:35 +08:00
onClick,
scrollElement,
setInitialScrollPosition,
showTime,
sortOrder,
timeRange,
timeZone,
wrapLogMessage,
}: Props) => {
const [infiniteLoaderState, setInfiniteLoaderState] = useState<InfiniteLoaderState>('idle');
const [autoScroll, setAutoScroll] = useState(false);
const prevLogs = usePrevious(logs);
const prevSortOrder = usePrevious(sortOrder);
const lastScroll = useRef<number>(scrollElement?.scrollTop || 0);
const lastEvent = useRef<Event | WheelEvent | null>(null);
const countRef = useRef(0);
const lastLogOfPage = useRef<string[]>([]);
const styles = useStyles2(getStyles);
useEffect(() => {
// Logs have not changed, ignore effect
if (!prevLogs || prevLogs === logs) {
return;
}
// New logs are from infinite scrolling
if (infiniteLoaderState === 'loading') {
// out-of-bounds if no new logs returned
setInfiniteLoaderState(logs.length === prevLogs.length ? 'out-of-bounds' : 'idle');
} else {
lastLogOfPage.current = [];
setAutoScroll(true);
}
}, [infiniteLoaderState, logs, prevLogs]);
useEffect(() => {
if (prevSortOrder && prevSortOrder !== sortOrder) {
setInfiniteLoaderState('idle');
}
}, [prevSortOrder, sortOrder]);
useEffect(() => {
if (autoScroll) {
setInitialScrollPosition();
setAutoScroll(false);
}
}, [autoScroll, setInitialScrollPosition]);
const onLoadMore = useCallback(() => {
const newRange = canScrollBottom(getVisibleRange(logs), timeRange, timeZone, sortOrder);
if (!newRange) {
setInfiniteLoaderState('out-of-bounds');
return;
}
lastLogOfPage.current.push(logs[logs.length - 1].uid);
setInfiniteLoaderState('loading');
loadMore?.(newRange);
reportInteraction('grafana_logs_infinite_scrolling', {
direction: 'bottom',
sort_order: sortOrder,
});
}, [loadMore, logs, sortOrder, timeRange, timeZone]);
useEffect(() => {
if (!scrollElement || !loadMore || !config.featureToggles.logsInfiniteScrolling) {
return;
}
function handleScroll(event: Event | WheelEvent) {
if (!scrollElement || !loadMore || !logs.length || infiniteLoaderState !== 'pre-scroll') {
return;
}
const scrollDirection = shouldLoadMore(event, lastEvent.current, countRef, scrollElement, lastScroll.current);
lastEvent.current = event;
lastScroll.current = scrollElement.scrollTop;
if (scrollDirection === ScrollDirection.Bottom) {
onLoadMore();
}
}
scrollElement.addEventListener('scroll', handleScroll);
scrollElement.addEventListener('wheel', handleScroll);
return () => {
scrollElement.removeEventListener('scroll', handleScroll);
scrollElement.removeEventListener('wheel', handleScroll);
};
}, [infiniteLoaderState, loadMore, logs.length, onLoadMore, scrollElement]);
const { t } = useTranslate();
const Renderer = useCallback(
({ index, style }: ListChildComponentProps) => {
if (!logs[index] && infiniteLoaderState !== 'idle') {
return (
<LogLineMessage
style={style}
styles={styles}
onClick={infiniteLoaderState === 'pre-scroll' ? onLoadMore : undefined}
>
{getMessageFromInfiniteLoaderState(infiniteLoaderState, sortOrder, t)}
</LogLineMessage>
);
}
return (
<LogLine
displayedFields={displayedFields}
index={index}
log={logs[index]}
New Logs Panel: Add Log Details support (#105609) * Log list: add onclick listener * LogListContext: add basic details support * LogLineDetails: create component * Address lint issues * Log Details: make resizable and store size * LogListModel: add sampled and error support * LogDetails: pass more required props * LogLineContext: add interactive callbacks support * LogLineDetails: pass interactive callbacks * LogList: pass displayedFields callbacks * LogLine: move click listener * LogLineMenu: support showing details * LogLine: move onclick listener * LogListContext: remove displayedFields intermediation * i18n * LogListContext: abstract details shown function * LogLine: visually show expanded lines * LogDetails: remove min width for labels * LogLineDetails: add close button * LogList: add extra wrapper to get width * LogLineDetails: update logs size on resize * virtualization: update to new width reference * LogLine: check overflow on every re-render * LogList: debug virtualization when resizing * LogLineDetails: make it scrollable * LogListContext: make detailsWidth not undefined * Update tests with new attributes * LogLine: update collapsed state with container changes * LogLine: move cursor property to clickable styles * LogList: fix height recalculation when display options change * Logs: fix feature toggles support * Logs: more feature toggles adjustments * Lint * LogLine: support duplicates, hasError, and isSampled * Logs: debug feature flag combinations * i18n * Prettier * New Logs Panel: generate storage key for dashboards * Explore Logs: fix filtered levels * Logs Sample: integrate new panel * LogLine: fix unwrapped logs * Fix test * Update test * Logs panel: update test * Prettier * LogLine: update tests * LogLineMenu: update test * LogList: update unit test * processing: update test * virtualization: update unit test
2025-05-21 01:28:35 +08:00
onClick={onClick}
showTime={showTime}
style={style}
styles={styles}
variant={getLogLineVariant(logs, index, lastLogOfPage.current)}
wrapLogMessage={wrapLogMessage}
onOverflow={handleOverflow}
/>
);
},
[
displayedFields,
handleOverflow,
infiniteLoaderState,
logs,
New Logs Panel: Add Log Details support (#105609) * Log list: add onclick listener * LogListContext: add basic details support * LogLineDetails: create component * Address lint issues * Log Details: make resizable and store size * LogListModel: add sampled and error support * LogDetails: pass more required props * LogLineContext: add interactive callbacks support * LogLineDetails: pass interactive callbacks * LogList: pass displayedFields callbacks * LogLine: move click listener * LogLineMenu: support showing details * LogLine: move onclick listener * LogListContext: remove displayedFields intermediation * i18n * LogListContext: abstract details shown function * LogLine: visually show expanded lines * LogDetails: remove min width for labels * LogLineDetails: add close button * LogList: add extra wrapper to get width * LogLineDetails: update logs size on resize * virtualization: update to new width reference * LogLine: check overflow on every re-render * LogList: debug virtualization when resizing * LogLineDetails: make it scrollable * LogListContext: make detailsWidth not undefined * Update tests with new attributes * LogLine: update collapsed state with container changes * LogLine: move cursor property to clickable styles * LogList: fix height recalculation when display options change * Logs: fix feature toggles support * Logs: more feature toggles adjustments * Lint * LogLine: support duplicates, hasError, and isSampled * Logs: debug feature flag combinations * i18n * Prettier * New Logs Panel: generate storage key for dashboards * Explore Logs: fix filtered levels * Logs Sample: integrate new panel * LogLine: fix unwrapped logs * Fix test * Update test * Logs panel: update test * Prettier * LogLine: update tests * LogLineMenu: update test * LogList: update unit test * processing: update test * virtualization: update unit test
2025-05-21 01:28:35 +08:00
onClick,
onLoadMore,
showTime,
sortOrder,
styles,
wrapLogMessage,
t,
]
);
const onItemsRendered = useCallback(
(props: ListOnItemsRenderedProps) => {
if (!scrollElement || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') {
return;
}
if (scrollElement.scrollHeight <= scrollElement.clientHeight) {
return;
}
const lastLogIndex = logs.length - 1;
const preScrollIndex = logs.length - 2;
if (props.visibleStopIndex >= lastLogIndex) {
setInfiniteLoaderState('pre-scroll');
} else if (props.visibleStartIndex < preScrollIndex) {
setInfiniteLoaderState('idle');
}
},
[infiniteLoaderState, logs.length, scrollElement]
);
const getItemKey = useCallback((index: number) => (logs[index] ? logs[index].uid : index.toString()), [logs]);
const itemCount = logs.length && loadMore && infiniteLoaderState !== 'idle' ? logs.length + 1 : logs.length;
return <>{children({ getItemKey, itemCount, onItemsRendered, Renderer })}</>;
};
function getMessageFromInfiniteLoaderState(state: InfiniteLoaderState, order: LogsSortOrder, t: TFunction) {
switch (state) {
case 'out-of-bounds':
return t('logs.infinite-scroll.end-of-range', 'End of the selected time range.');
case 'loading':
return (
<>
{order === LogsSortOrder.Ascending
? t('logs.infinite-scroll.load-newer', 'Loading newer logs...')
: t('logs.infinite-scroll.load-older', 'Loading older logs...')}{' '}
<Spinner inline />
</>
);
case 'pre-scroll':
return t('logs.infinite-scroll.load-more', 'Scroll to load more');
default:
return null;
}
}
function getLogLineVariant(logs: LogListModel[], index: number, lastLogOfPage: string[]) {
if (!lastLogOfPage.length || !logs[index - 1]) {
return undefined;
}
const prevLog = logs[index - 1];
for (const uid of lastLogOfPage) {
if (prevLog.uid === uid) {
// First log of an infinite scrolling page
return 'infinite-scroll';
}
}
return undefined;
}