From 0f56441b8bd74a868ac54c4181f694518e788aff Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 1 Dec 2025 16:11:36 +0000 Subject: [PATCH] upgrade Logs panel --- .../components/panel/InfiniteScroll.test.tsx | 28 +++--- .../logs/components/panel/InfiniteScroll.tsx | 20 ++-- .../logs/components/panel/LogList.tsx | 98 +++++++++++-------- .../logs/components/panel/LogListSearch.tsx | 19 +++- 4 files changed, 97 insertions(+), 68 deletions(-) diff --git a/public/app/features/logs/components/panel/InfiniteScroll.test.tsx b/public/app/features/logs/components/panel/InfiniteScroll.test.tsx index 2507a3b5176..356ffe12dd5 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.test.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.test.tsx @@ -1,5 +1,5 @@ import { act, render, screen } from '@testing-library/react'; -import { VariableSizeList } from 'react-window'; +import { List } from 'react-window'; import { createTheme, dateTimeForTimeZone, rangeUtil } from '@grafana/data'; import { LogsSortOrder } from '@grafana/schema'; @@ -89,19 +89,19 @@ function setup( loadMore={loadMoreMock} infiniteScrollMode={infiniteScrollMode} > - {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( - virtualization.getLineHeight()} - itemKey={getItemKey} - layout="vertical" - onItemsRendered={onItemsRendered} - style={{ overflow: 'scroll' }} - width="100%" - > - {Renderer} - + {({ itemCount, onItemsRendered, Renderer }) => ( + virtualization.getLineHeight()} + onRowsRendered={onItemsRendered} + style={{ + overflow: 'scroll', + height: 100, + width: '100%', + }} + /> )} ); diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index 3c7de64b6be..c662e49d1d8 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -1,6 +1,6 @@ -import { ReactNode, useCallback, useEffect, useRef, useState, MouseEvent } from 'react'; +import { ReactNode, useCallback, useEffect, useRef, useState, MouseEvent, type JSX } from 'react'; import { usePrevious } from 'react-use'; -import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window'; +import { type RowComponentProps, type ListProps } from 'react-window'; import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -17,8 +17,8 @@ import { LogLineVirtualization } from './virtualization'; interface ChildrenProps { itemCount: number; getItemKey: (index: number) => string; - onItemsRendered: (props: ListOnItemsRenderedProps) => void; - Renderer: (props: ListChildComponentProps) => ReactNode; + onItemsRendered: ListProps<{}>['onRowsRendered']; + Renderer: (props: RowComponentProps) => JSX.Element; } export interface Props { @@ -200,7 +200,7 @@ export const InfiniteScroll = ({ }, [onLoadMore]); const Renderer = useCallback( - ({ index, style }: ListChildComponentProps) => { + ({ index, style }: RowComponentProps) => { if (!logs[index] && infiniteLoaderState !== 'idle') { return ( { + const onItemsRendered = useCallback['onRowsRendered']>>( + (props) => { if (!scrollElement) { return; } - if (props.visibleStartIndex === 0) { + if (props.startIndex === 0) { noScrollRef.current = scrollElement.scrollHeight <= scrollElement.clientHeight; } if (noScrollRef.current || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') { @@ -261,9 +261,9 @@ export const InfiniteScroll = ({ } const lastLogIndex = logs.length - 1; const preScrollIndex = logs.length - 2; - if (props.visibleStopIndex >= lastLogIndex) { + if (props.stopIndex >= lastLogIndex) { setInfiniteLoaderState('pre-scroll-bottom'); - } else if (props.visibleStartIndex < preScrollIndex) { + } else if (props.startIndex < preScrollIndex) { setInfiniteLoaderState('idle'); } }, diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index 8a8a9b085dc..35f7880544d 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { debounce } from 'lodash'; import { Grammar } from 'prismjs'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, MouseEvent } from 'react'; -import { Align, VariableSizeList } from 'react-window'; +import { Align, List, ListImperativeAPI, useListRef } from 'react-window'; import { CoreApp, @@ -282,10 +282,9 @@ const LogListComponent = ({ const [processedLogs, setProcessedLogs] = useState([]); const [listHeight, setListHeight] = useState(getListHeight(containerElement, app)); const theme = useTheme2(); - const listRef = useRef(null); + const listRef = useListRef(null); const widthRef = useRef(containerElement.clientWidth); const wrapperRef = useRef(null); - const scrollRef = useRef(null); const virtualization = useMemo(() => new LogLineVirtualization(theme, fontSize), [theme, fontSize]); const dimensions = useMemo( () => @@ -329,25 +328,29 @@ const LogListComponent = ({ // When log lines report size discrepancies, we debounce the calculation reset to give time to // use the smallest log index to reset the heights. - const debouncedResetAfterIndex = useMemo(() => { - return debounce((index: number) => { - listRef.current?.resetAfterIndex(index); - overflowIndexRef.current = Infinity; - }, 0); - }, []); + // TODO do we need this? + // const debouncedResetAfterIndex = useMemo(() => { + // return debounce((index: number) => { + // listRef.current?.resetAfterIndex(index); + // overflowIndexRef.current = Infinity; + // }, 0); + // }, []); const debouncedScrollToItem = useMemo(() => { return debounce((index: number, align?: Align) => { - listRef.current?.scrollToItem(index, align); + listRef.current?.scrollToRow({ + index, + align, + }); }, 250); - }, []); + }, [listRef]); useEffect(() => { const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) => handleScrollToEvent(e, filteredLogs, listRef.current) ); return () => subscription.unsubscribe(); - }, [eventBus, filteredLogs]); + }, [eventBus, filteredLogs, listRef]); useEffect(() => { setProcessedLogs( @@ -366,11 +369,13 @@ const LogListComponent = ({ ) ); virtualization.resetLogLineSizes(); - listRef.current?.resetAfterIndex(0); + // TODO do we need this? + // listRef.current?.resetAfterIndex(0); }, [forceEscape, getFieldLinks, grammar, logs, prettifyJSON, sortOrder, timeZone, virtualization, wrapLogMessage]); useEffect(() => { - listRef.current?.resetAfterIndex(0); + // TODO do we need this? + // listRef.current?.resetAfterIndex(0); }, [wrapLogMessage, showDetails, displayedFields, dedupStrategy]); useLayoutEffect(() => { @@ -399,9 +404,10 @@ const LogListComponent = ({ return; } overflowIndexRef.current = index < overflowIndexRef.current ? index : overflowIndexRef.current; - debouncedResetAfterIndex(overflowIndexRef.current); + // TODO do we need this? + // debouncedResetAfterIndex(overflowIndexRef.current); }, - [debouncedResetAfterIndex, virtualization, widthContainer] + [virtualization, widthContainer] ); const handleScrollPosition = useCallback( @@ -410,13 +416,13 @@ const LogListComponent = ({ if (scrollToUID) { const index = processedLogs.findIndex((log) => log.uid === scrollToUID); if (index >= 0) { - listRef.current?.scrollToItem(index, 'start'); + listRef.current?.scrollToRow({ index, align: 'start' }); return; } } - listRef.current?.scrollToItem(initialScrollPosition === 'top' ? 0 : processedLogs.length - 1); + listRef.current?.scrollToRow({ index: initialScrollPosition === 'top' ? 0 : processedLogs.length - 1 }); }, - [initialScrollPosition, permalinkedLogId, processedLogs] + [initialScrollPosition, permalinkedLogId, processedLogs, listRef] ); const handleLogLineClick = useCallback( @@ -503,7 +509,7 @@ const LogListComponent = ({ logs={filteredLogs} loadMore={loadMore} onClick={handleLogLineClick} - scrollElement={scrollRef.current} + scrollElement={listRef.current?.element ?? null} showTime={showTime} sortOrder={sortOrder} timeRange={timeRange} @@ -512,12 +518,25 @@ const LogListComponent = ({ virtualization={virtualization} wrapLogMessage={wrapLogMessage} > - {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( - ( + - {Renderer} - + listRef={listRef} + /> )} @@ -586,16 +599,23 @@ function getStyles( }; } -function handleScrollToEvent(event: ScrollToLogsEvent, logs: LogListModel[], list: VariableSizeList | null) { +function handleScrollToEvent(event: ScrollToLogsEvent, logs: LogListModel[], list: ListImperativeAPI | null) { if (event.payload.scrollTo === 'top') { - list?.scrollTo(0); + list?.scrollToRow({ + index: 0, + }); } else if (event.payload.scrollTo === 'bottom') { - list?.scrollToItem(logs.length - 1); + list?.scrollToRow({ + index: logs.length - 1, + }); } else { // uid const index = logs.findIndex((log) => log.uid === event.payload.scrollTo); if (index >= 0) { - list?.scrollToItem(index, 'center'); + list?.scrollToRow({ + index, + align: 'center', + }); } } } diff --git a/public/app/features/logs/components/panel/LogListSearch.tsx b/public/app/features/logs/components/panel/LogListSearch.tsx index 3cdd1cbbe3d..353dd99cbe2 100644 --- a/public/app/features/logs/components/panel/LogListSearch.tsx +++ b/public/app/features/logs/components/panel/LogListSearch.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { ChangeEvent, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { VariableSizeList } from 'react-window'; +import { type ListImperativeAPI } from 'react-window'; import { escapeRegex, GrafanaTheme2, shallowCompare } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -12,7 +12,7 @@ import { useLogListSearchContext } from './LogListSearchContext'; import { LogListModel } from './processing'; interface Props { - listRef: VariableSizeList | null; + listRef: ListImperativeAPI | null; logs: LogListModel[]; } @@ -61,7 +61,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => { } const prev = currentResult > 0 ? currentResult - 1 : matches.length - 1; setCurrentResult(prev); - listRef?.scrollToItem(logs.indexOf(matches[prev]), 'center'); + listRef?.scrollToRow({ + index: logs.indexOf(matches[prev]), + align: 'center', + }); }, [currentResult, listRef, logs, matches]); const nextResult = useCallback(() => { @@ -70,7 +73,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => { } const next = currentResult < matches.length - 1 ? currentResult + 1 : 0; setCurrentResult(next); - listRef?.scrollToItem(logs.indexOf(matches[next]), 'center'); + listRef?.scrollToRow({ + index: logs.indexOf(matches[next]), + align: 'center', + }); }, [currentResult, listRef, logs, matches]); useEffect(() => { @@ -80,7 +86,10 @@ export const LogListSearch = ({ listRef, logs }: Props) => { } if (!currentResult) { setCurrentResult(0); - listRef?.scrollToItem(logs.indexOf(matches[0]), 'center'); + listRef?.scrollToRow({ + index: logs.indexOf(matches[0]), + align: 'center', + }); } }, [currentResult, listRef, logs, matches]);