New Logs Panel: Performance and display updates and fixes (#109672)

* LogLine: don't run overflow check with unwrapped logs

* InfiniteScroll: don't listen to scroll events without scroll

* InfiniteScroll: minimize use of element.scrollHeight

* LogLineDetails: constrain to 95vw

* LogList: process logs while streaming

* Remove unused prop

* InfiniteScroll: update test

* Update test

* Inline Log Details: read max width from context

* Remove console
This commit is contained in:
Matias Chomicki
2025-08-18 19:06:31 +00:00
committed by GitHub
parent bb55972cef
commit 9a6b012ea4
9 changed files with 56 additions and 38 deletions
@@ -57,10 +57,9 @@ function setup(
startPosition: number,
rows: LogRowModel[],
order: LogsSortOrder,
app?: CoreApp
app?: CoreApp,
{ element, events } = getMockElement(startPosition)
) {
const { element, events } = getMockElement(startPosition);
function scrollTo(position: number, timeStamp?: number) {
element.scrollTop = position;
@@ -69,7 +68,7 @@ function setup(
if (timeStamp) {
jest.spyOn(event, 'timeStamp', 'get').mockReturnValue(timeStamp);
}
events['scroll'](event);
events['scroll']?.(event);
});
// When scrolling top, we wait for the user to reach the top, and then for a new scrolling event
@@ -192,12 +191,14 @@ describe('InfiniteScroll', () => {
test('Does not request more logs when there is no scroll', async () => {
const loadMoreMock = jest.fn();
const { scrollTo, element } = setup(loadMoreMock, 0, rows, order);
expect(await screen.findByTestId('contents')).toBeInTheDocument();
const { element, events } = getMockElement(0);
element.clientHeight = 40;
element.scrollHeight = element.clientHeight;
const { scrollTo } = setup(loadMoreMock, 0, rows, order, undefined, { element, events });
expect(await screen.findByTestId('contents')).toBeInTheDocument();
scrollTo(39, 1);
scrollTo(40, 600);
@@ -81,6 +81,9 @@ export const InfiniteScroll = ({
if (!scrollElement || !loadMoreLogs) {
return;
}
if (scrollElement.scrollHeight <= scrollElement.clientHeight) {
return;
}
function handleScroll(event: Event | WheelEvent) {
if (!scrollElement || !loadMoreLogs || !rows.length || loading || !config.featureToggles.logsInfiniteScrolling) {
@@ -227,10 +230,6 @@ export function shouldLoadMore(
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;
@@ -46,10 +46,9 @@ function setup(
startPosition: number,
logs: LogListModel[],
order: LogsSortOrder,
infiniteScrollMode: InfiniteScrollMode = 'interval'
infiniteScrollMode: InfiniteScrollMode = 'interval',
{ element, events } = getMockElement(startPosition)
) {
const { element, events } = getMockElement(startPosition);
function scrollTo(position: number, timeStamp?: number) {
element.scrollTop = position;
@@ -168,12 +167,14 @@ describe('InfiniteScroll', () => {
test('Does not request more logs when there is no scroll', async () => {
const loadMoreMock = jest.fn();
const { scrollTo, element } = setup(loadMoreMock, 0, logs, order);
expect(await screen.findByText('log line 1')).toBeInTheDocument();
const { element, events } = getMockElement(0);
element.clientHeight = 40;
element.scrollHeight = element.clientHeight;
const { scrollTo } = setup(loadMoreMock, 0, logs, order, undefined, { element, events });
expect(await screen.findByText('log line 1')).toBeInTheDocument();
scrollTo(39, 1);
scrollTo(40, 600);
@@ -73,6 +73,7 @@ export const InfiniteScroll = ({
const styles = useStyles2(getStyles, virtualization);
const resetStateTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const scrollToLogLineRef = useRef<LogListModel | undefined>(undefined);
const noScrollRef = useRef(false);
useEffect(() => {
// Logs have not changed, ignore effect
@@ -237,10 +238,13 @@ export const InfiniteScroll = ({
const onItemsRendered = useCallback(
(props: ListOnItemsRenderedProps) => {
if (!scrollElement || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') {
if (!scrollElement) {
return;
}
if (scrollElement.scrollHeight <= scrollElement.clientHeight) {
if (props.visibleStartIndex === 0) {
noScrollRef.current = scrollElement.scrollHeight <= scrollElement.clientHeight;
}
if (noScrollRef.current || infiniteLoaderState === 'loading' || infiniteLoaderState === 'out-of-bounds') {
return;
}
const lastLogIndex = logs.length - 1;
@@ -110,7 +110,7 @@ const LogLineComponent = memo(
const permalinked = useLogIsPermalinked(log);
useEffect(() => {
if (!onOverflow || !logLineRef.current || !virtualization || !height) {
if (!onOverflow || !logLineRef.current || !virtualization || !height || !wrapLogMessage) {
return;
}
const calculatedHeight = typeof height === 'number' ? height : undefined;
@@ -134,7 +134,7 @@ export interface InlineLogLineDetailsProps {
}
export const InlineLogLineDetails = memo(({ logs, log }: InlineLogLineDetailsProps) => {
const { app, noInteractions } = useLogListContext();
const { app, detailsWidth, noInteractions } = useLogListContext();
const styles = useStyles2(getStyles, 'inline');
const scrollRef = useRef<HTMLDivElement | null>(null);
@@ -159,7 +159,7 @@ export const InlineLogLineDetails = memo(({ logs, log }: InlineLogLineDetailsPro
}, [log]);
return (
<div className={`${styles.inlineWrapper} log-line-inline-details`}>
<div className={`${styles.inlineWrapper} log-line-inline-details`} style={{ maxWidth: detailsWidth }}>
<div className={styles.container}>
<div className={styles.scrollContainer} ref={scrollRef} onScroll={saveScroll}>
<LogLineDetailsComponent log={log} logs={logs} />
@@ -96,6 +96,7 @@ type LogListComponentProps = Omit<
| 'dedupStrategy'
| 'displayedFields'
| 'enableLogDetails'
| 'loading'
| 'logOptionsStorageKey'
| 'permalinkedLogId'
| 'showTime'
@@ -202,7 +203,6 @@ export const LogList = ({
grammar={grammar}
initialScrollPosition={initialScrollPosition}
infiniteScrollMode={infiniteScrollMode}
loading={loading}
loadMore={loadMore}
logs={logs}
showControls={showControls}
@@ -221,7 +221,6 @@ const LogListComponent = ({
grammar,
initialScrollPosition = 'top',
infiniteScrollMode = 'interval',
loading,
loadMore,
logs,
showControls,
@@ -312,9 +311,6 @@ const LogListComponent = ({
}, [eventBus, filteredLogs]);
useEffect(() => {
if (loading) {
return;
}
setProcessedLogs(
preProcessLogs(
logs,
@@ -324,7 +320,7 @@ const LogListComponent = ({
);
virtualization.resetLogLineSizes();
listRef.current?.resetAfterIndex(0);
}, [forceEscape, getFieldLinks, grammar, loading, logs, sortOrder, timeZone, virtualization, wrapLogMessage]);
}, [forceEscape, getFieldLinks, grammar, logs, sortOrder, timeZone, virtualization, wrapLogMessage]);
useEffect(() => {
listRef.current?.resetAfterIndex(0);
@@ -40,7 +40,7 @@ import { LogLineDetailsMode } from './LogLineDetails';
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
import { LogListFontSize } from './LogList';
import { LogListModel } from './processing';
import { LOG_LIST_MIN_WIDTH } from './virtualization';
import { getScrollbarWidth, LOG_LIST_CONTROLS_WIDTH, LOG_LIST_MIN_WIDTH } from './virtualization';
export interface LogListContextData extends Omit<Props, 'containerElement' | 'logs' | 'logsMeta' | 'showControls'> {
closeDetails: () => void;
@@ -259,7 +259,9 @@ export const LogListContextProvider = ({
wrapLogMessage,
});
const [showDetails, setShowDetails] = useState<LogListModel[]>([]);
const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey));
const [detailsWidth, setDetailsWidthState] = useState(
getDetailsWidth(containerElement, logOptionsStorageKey, undefined, detailsModeProp, showControls)
);
const [detailsMode, setDetailsMode] = useState<LogLineDetailsMode>(detailsModeProp ?? 'sidebar');
const [isAssistantAvailable, openAssistant] = useAssistant();
@@ -361,17 +363,25 @@ export const LogListContextProvider = ({
}
}, [logs, showDetails]);
// Sync log details inline and sidebar width
useEffect(() => {
setDetailsWidthState(getDetailsWidth(containerElement, logOptionsStorageKey, undefined, detailsMode, showControls));
}, [containerElement, detailsMode, logOptionsStorageKey, showControls]);
// Sync log details width
useEffect(() => {
if (!containerElement) {
return;
}
const handleResize = debounce(() => {
setDetailsWidthState((detailsWidth) => getDetailsWidth(containerElement, logOptionsStorageKey, detailsWidth));
setDetailsWidthState((detailsWidth) =>
getDetailsWidth(containerElement, logOptionsStorageKey, detailsWidth, detailsMode, showControls)
);
}, 50);
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [containerElement, logOptionsStorageKey]);
const observer = new ResizeObserver(() => handleResize());
observer.observe(containerElement);
return () => observer.disconnect();
}, [containerElement, detailsMode, logOptionsStorageKey, showControls]);
// Sync timestamp resolution
useEffect(() => {
@@ -665,11 +675,16 @@ export function isDedupStrategy(value: unknown): value is LogsDedupStrategy {
function getDetailsWidth(
containerElement: HTMLDivElement | undefined,
logOptionsStorageKey?: string,
currentWidth?: number
currentWidth?: number,
detailsMode: LogLineDetailsMode = 'sidebar',
showControls?: boolean
) {
if (!containerElement) {
return 0;
}
if (detailsMode === 'inline') {
return containerElement.clientWidth - getScrollbarWidth() - (showControls ? LOG_LIST_CONTROLS_WIDTH : 0);
}
const defaultWidth = containerElement.clientWidth * 0.4;
const detailsWidth =
currentWidth ||
@@ -13,6 +13,8 @@ export const FIELD_GAP_MULTIPLIER = 1.5;
export const DEFAULT_LINE_HEIGHT = 22;
export const LOG_LIST_CONTROLS_WIDTH = 32;
export class LogLineVirtualization {
private ctx: CanvasRenderingContext2D | null = null;
private gridSize;