2025-02-27 18:31:55 +08:00
|
|
|
import { css } from '@emotion/css';
|
2025-02-05 01:40:17 +08:00
|
|
|
import { debounce } from 'lodash';
|
2025-02-27 18:31:55 +08:00
|
|
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
2025-02-14 19:52:34 +08:00
|
|
|
import { VariableSizeList } from 'react-window';
|
2025-02-05 01:40:17 +08:00
|
|
|
|
2025-02-27 18:31:55 +08:00
|
|
|
import {
|
|
|
|
AbsoluteTimeRange,
|
|
|
|
CoreApp,
|
|
|
|
DataFrame,
|
|
|
|
EventBus,
|
|
|
|
Field,
|
|
|
|
LinkModel,
|
|
|
|
LogRowModel,
|
|
|
|
LogsSortOrder,
|
|
|
|
TimeRange,
|
|
|
|
} from '@grafana/data';
|
2025-02-28 00:34:02 +08:00
|
|
|
import { PopoverContent, useTheme2 } from '@grafana/ui';
|
2025-02-05 01:40:17 +08:00
|
|
|
|
2025-02-14 19:52:34 +08:00
|
|
|
import { InfiniteScroll } from './InfiniteScroll';
|
2025-02-27 18:31:55 +08:00
|
|
|
import { getGridTemplateColumns } from './LogLine';
|
2025-02-28 00:34:02 +08:00
|
|
|
import { GetRowContextQueryFn } from './LogLineMenu';
|
|
|
|
import { LogListContext } from './LogListContext';
|
2025-02-27 18:31:55 +08:00
|
|
|
import { preProcessLogs, LogListModel, calculateFieldDimensions, LogFieldDimension } from './processing';
|
2025-02-05 01:40:17 +08:00
|
|
|
import {
|
|
|
|
getLogLineSize,
|
|
|
|
init as initVirtualization,
|
|
|
|
resetLogLineSizes,
|
|
|
|
ScrollToLogsEvent,
|
|
|
|
storeLogLineSize,
|
|
|
|
} from './virtualization';
|
|
|
|
|
2025-02-27 18:31:55 +08:00
|
|
|
export type GetFieldLinksFn = (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
|
|
|
|
2025-02-05 01:40:17 +08:00
|
|
|
interface Props {
|
|
|
|
app: CoreApp;
|
|
|
|
containerElement: HTMLDivElement;
|
2025-02-27 18:31:55 +08:00
|
|
|
displayedFields: string[];
|
2025-02-05 01:40:17 +08:00
|
|
|
eventBus: EventBus;
|
|
|
|
forceEscape?: boolean;
|
2025-02-27 18:31:55 +08:00
|
|
|
getFieldLinks?: GetFieldLinksFn;
|
2025-02-28 00:34:02 +08:00
|
|
|
getRowContextQuery?: GetRowContextQueryFn;
|
2025-02-14 19:52:34 +08:00
|
|
|
initialScrollPosition?: 'top' | 'bottom';
|
|
|
|
loadMore?: (range: AbsoluteTimeRange) => void;
|
2025-02-27 18:31:55 +08:00
|
|
|
logs: LogRowModel[];
|
2025-02-28 00:34:02 +08:00
|
|
|
logSupportsContext?: (row: LogRowModel) => boolean;
|
|
|
|
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
|
|
|
onPinLine?: (row: LogRowModel) => void;
|
|
|
|
onOpenContext?: (row: LogRowModel, onClose: () => void) => void;
|
|
|
|
onUnpinLine?: (row: LogRowModel) => void;
|
|
|
|
pinLineButtonTooltipTitle?: PopoverContent;
|
|
|
|
pinnedLogs?: string[];
|
2025-02-05 01:40:17 +08:00
|
|
|
showTime: boolean;
|
|
|
|
sortOrder: LogsSortOrder;
|
2025-02-14 19:52:34 +08:00
|
|
|
timeRange: TimeRange;
|
2025-02-05 01:40:17 +08:00
|
|
|
timeZone: string;
|
|
|
|
wrapLogMessage: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const LogList = ({
|
|
|
|
app,
|
|
|
|
containerElement,
|
2025-02-27 18:31:55 +08:00
|
|
|
displayedFields = [],
|
2025-02-05 01:40:17 +08:00
|
|
|
eventBus,
|
|
|
|
forceEscape = false,
|
2025-02-27 18:31:55 +08:00
|
|
|
getFieldLinks,
|
2025-02-14 19:52:34 +08:00
|
|
|
initialScrollPosition = 'top',
|
|
|
|
loadMore,
|
|
|
|
logs,
|
2025-02-05 01:40:17 +08:00
|
|
|
showTime,
|
|
|
|
sortOrder,
|
2025-02-14 19:52:34 +08:00
|
|
|
timeRange,
|
2025-02-05 01:40:17 +08:00
|
|
|
timeZone,
|
|
|
|
wrapLogMessage,
|
2025-02-28 00:34:02 +08:00
|
|
|
...logListContext
|
2025-02-05 01:40:17 +08:00
|
|
|
}: Props) => {
|
2025-02-14 19:52:34 +08:00
|
|
|
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
|
2025-02-05 01:40:17 +08:00
|
|
|
const [listHeight, setListHeight] = useState(
|
|
|
|
app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight
|
|
|
|
);
|
|
|
|
const theme = useTheme2();
|
|
|
|
const listRef = useRef<VariableSizeList | null>(null);
|
|
|
|
const widthRef = useRef(containerElement.clientWidth);
|
2025-02-14 19:52:34 +08:00
|
|
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
2025-02-27 18:31:55 +08:00
|
|
|
const dimensions = useMemo(
|
|
|
|
() => (wrapLogMessage ? [] : calculateFieldDimensions(processedLogs, displayedFields)),
|
|
|
|
[displayedFields, processedLogs, wrapLogMessage]
|
|
|
|
);
|
|
|
|
const styles = getStyles(dimensions, { showTime });
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
initVirtualization(theme);
|
|
|
|
}, [theme]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-02-14 19:52:34 +08:00
|
|
|
const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) =>
|
|
|
|
handleScrollToEvent(e, logs.length, listRef.current)
|
|
|
|
);
|
2025-02-05 01:40:17 +08:00
|
|
|
return () => subscription.unsubscribe();
|
2025-02-14 19:52:34 +08:00
|
|
|
}, [eventBus, logs.length]);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
2025-02-27 18:31:55 +08:00
|
|
|
setProcessedLogs(
|
|
|
|
preProcessLogs(logs, { getFieldLinks, wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone })
|
|
|
|
);
|
2025-02-05 01:40:17 +08:00
|
|
|
listRef.current?.resetAfterIndex(0);
|
2025-02-27 18:31:55 +08:00
|
|
|
}, [forceEscape, getFieldLinks, logs, sortOrder, timeZone, wrapLogMessage]);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const handleResize = debounce(() => {
|
|
|
|
setListHeight(app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight);
|
|
|
|
}, 50);
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
handleResize();
|
|
|
|
return () => {
|
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
|
};
|
|
|
|
}, [app, containerElement.clientHeight]);
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
if (widthRef.current === containerElement.clientWidth) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
resetLogLineSizes();
|
|
|
|
listRef.current?.resetAfterIndex(0);
|
|
|
|
widthRef.current = containerElement.clientWidth;
|
|
|
|
});
|
|
|
|
|
|
|
|
const handleOverflow = useCallback(
|
|
|
|
(index: number, id: string, height: number) => {
|
|
|
|
if (containerElement) {
|
|
|
|
storeLogLineSize(id, containerElement, height);
|
|
|
|
listRef.current?.resetAfterIndex(index);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[containerElement]
|
|
|
|
);
|
|
|
|
|
2025-02-14 19:52:34 +08:00
|
|
|
const handleScrollPosition = useCallback(() => {
|
|
|
|
listRef.current?.scrollToItem(initialScrollPosition === 'top' ? 0 : logs.length - 1);
|
|
|
|
}, [initialScrollPosition, logs.length]);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
if (!containerElement || listHeight == null) {
|
|
|
|
// Wait for container to be rendered
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2025-02-28 00:34:02 +08:00
|
|
|
<LogListContext.Provider value={logListContext}>
|
|
|
|
<InfiniteScroll
|
|
|
|
displayedFields={displayedFields}
|
|
|
|
handleOverflow={handleOverflow}
|
|
|
|
logs={processedLogs}
|
|
|
|
loadMore={loadMore}
|
|
|
|
scrollElement={scrollRef.current}
|
|
|
|
showTime={showTime}
|
|
|
|
sortOrder={sortOrder}
|
|
|
|
timeRange={timeRange}
|
|
|
|
timeZone={timeZone}
|
|
|
|
setInitialScrollPosition={handleScrollPosition}
|
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
>
|
|
|
|
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => (
|
|
|
|
<VariableSizeList
|
|
|
|
className={styles.logList}
|
|
|
|
height={listHeight}
|
|
|
|
itemCount={itemCount}
|
|
|
|
itemSize={getLogLineSize.bind(null, processedLogs, containerElement, displayedFields, {
|
|
|
|
wrap: wrapLogMessage,
|
|
|
|
showTime,
|
|
|
|
})}
|
|
|
|
itemKey={getItemKey}
|
|
|
|
layout="vertical"
|
|
|
|
onItemsRendered={onItemsRendered}
|
|
|
|
outerRef={scrollRef}
|
|
|
|
ref={listRef}
|
|
|
|
style={{ overflowY: 'scroll' }}
|
|
|
|
width="100%"
|
|
|
|
>
|
|
|
|
{Renderer}
|
|
|
|
</VariableSizeList>
|
|
|
|
)}
|
|
|
|
</InfiniteScroll>
|
|
|
|
</LogListContext.Provider>
|
2025-02-05 01:40:17 +08:00
|
|
|
);
|
|
|
|
};
|
2025-02-14 19:52:34 +08:00
|
|
|
|
2025-02-27 18:31:55 +08:00
|
|
|
function getStyles(dimensions: LogFieldDimension[], { showTime }: { showTime: boolean }) {
|
|
|
|
const columns = showTime ? dimensions : dimensions.filter((_, index) => index > 0);
|
|
|
|
return {
|
|
|
|
logList: css({
|
|
|
|
'& .unwrapped-log-line': {
|
|
|
|
display: 'grid',
|
|
|
|
gridTemplateColumns: getGridTemplateColumns(columns),
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2025-02-14 19:52:34 +08:00
|
|
|
function handleScrollToEvent(event: ScrollToLogsEvent, logsCount: number, list: VariableSizeList | null) {
|
|
|
|
if (event.payload.scrollTo === 'top') {
|
|
|
|
list?.scrollTo(0);
|
|
|
|
} else {
|
|
|
|
list?.scrollToItem(logsCount - 1);
|
|
|
|
}
|
|
|
|
}
|