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-04-04 20:53:12 +08:00
|
|
|
import { Grammar } from 'prismjs';
|
2025-06-10 17:59:01 +08:00
|
|
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, MouseEvent } from 'react';
|
2025-07-08 17:57:28 +08:00
|
|
|
import { Align, VariableSizeList } from 'react-window';
|
2025-02-05 01:40:17 +08:00
|
|
|
|
2025-02-27 18:31:55 +08:00
|
|
|
import {
|
|
|
|
CoreApp,
|
2025-05-21 01:28:35 +08:00
|
|
|
DataFrame,
|
2025-02-27 18:31:55 +08:00
|
|
|
EventBus,
|
2025-04-04 20:53:12 +08:00
|
|
|
EventBusSrv,
|
2025-06-10 17:59:01 +08:00
|
|
|
GrafanaTheme2,
|
2025-04-04 20:53:12 +08:00
|
|
|
LogLevel,
|
2025-02-27 18:31:55 +08:00
|
|
|
LogRowModel,
|
2025-04-04 20:53:12 +08:00
|
|
|
LogsDedupStrategy,
|
2025-04-07 22:38:55 +08:00
|
|
|
LogsMetaItem,
|
2025-02-27 18:31:55 +08:00
|
|
|
LogsSortOrder,
|
2025-04-07 22:38:55 +08:00
|
|
|
store,
|
2025-02-27 18:31:55 +08:00
|
|
|
TimeRange,
|
|
|
|
} from '@grafana/data';
|
2025-06-12 17:03:52 +08:00
|
|
|
import { Trans, t } from '@grafana/i18n';
|
2025-06-21 20:38:46 +08:00
|
|
|
import { ConfirmModal, Icon, PopoverContent, useStyles2, useTheme2 } from '@grafana/ui';
|
2025-06-10 17:59:01 +08:00
|
|
|
import { PopoverMenu } from 'app/features/explore/Logs/PopoverMenu';
|
2025-04-15 22:08:29 +08:00
|
|
|
import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
|
2025-02-05 01:40:17 +08:00
|
|
|
|
2025-08-01 19:30:17 +08:00
|
|
|
import { InfiniteScrollMode, InfiniteScroll, LoadMoreLogsType } from './InfiniteScroll';
|
2025-08-09 01:13:01 +08:00
|
|
|
import { getGridTemplateColumns, LogLineTimestampResolution } from './LogLine';
|
2025-07-09 22:07:19 +08:00
|
|
|
import { LogLineDetails, LogLineDetailsMode } from './LogLineDetails';
|
2025-06-01 20:29:49 +08:00
|
|
|
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
|
2025-04-04 20:53:12 +08:00
|
|
|
import { LogListContextProvider, LogListState, useLogListContext } from './LogListContext';
|
|
|
|
import { LogListControls } from './LogListControls';
|
2025-06-13 20:50:03 +08:00
|
|
|
import { LOG_LIST_SEARCH_HEIGHT, LogListSearch } from './LogListSearch';
|
|
|
|
import { LogListSearchContextProvider, useLogListSearchContext } from './LogListSearchContext';
|
2025-09-19 17:29:26 +08:00
|
|
|
import { preProcessLogs, LogListModel, getLevelsFromLogs } from './processing';
|
2025-06-13 20:50:03 +08:00
|
|
|
import { useKeyBindings } from './useKeyBindings';
|
2025-06-10 17:59:01 +08:00
|
|
|
import { usePopoverMenu } from './usePopoverMenu';
|
2025-06-21 20:38:46 +08:00
|
|
|
import { LogLineVirtualization, getLogLineSize, LogFieldDimension, ScrollToLogsEvent } from './virtualization';
|
2025-02-05 01:40:17 +08:00
|
|
|
|
2025-05-21 01:28:35 +08:00
|
|
|
export interface Props {
|
2025-02-05 01:40:17 +08:00
|
|
|
app: CoreApp;
|
|
|
|
containerElement: HTMLDivElement;
|
2025-04-04 20:53:12 +08:00
|
|
|
dedupStrategy: LogsDedupStrategy;
|
2025-07-09 22:07:19 +08:00
|
|
|
detailsMode?: LogLineDetailsMode;
|
2025-02-27 18:31:55 +08:00
|
|
|
displayedFields: string[];
|
2025-05-21 01:28:35 +08:00
|
|
|
enableLogDetails: boolean;
|
2025-04-04 20:53:12 +08:00
|
|
|
eventBus?: EventBus;
|
|
|
|
filterLevels?: LogLevel[];
|
2025-06-10 17:59:01 +08:00
|
|
|
fontSize?: LogListFontSize;
|
2025-02-27 18:31:55 +08:00
|
|
|
getFieldLinks?: GetFieldLinksFn;
|
2025-02-28 00:34:02 +08:00
|
|
|
getRowContextQuery?: GetRowContextQueryFn;
|
2025-04-04 20:53:12 +08:00
|
|
|
grammar?: Grammar;
|
2025-08-01 19:30:17 +08:00
|
|
|
infiniteScrollMode?: InfiniteScrollMode;
|
2025-02-14 19:52:34 +08:00
|
|
|
initialScrollPosition?: 'top' | 'bottom';
|
2025-05-21 01:28:35 +08:00
|
|
|
isLabelFilterActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
2025-05-07 06:26:54 +08:00
|
|
|
loading?: boolean;
|
2025-08-01 19:30:17 +08:00
|
|
|
loadMore?: LoadMoreLogsType;
|
2025-06-01 20:29:49 +08:00
|
|
|
logLineMenuCustomItems?: LogLineMenuCustomItem[];
|
2025-04-04 20:53:12 +08:00
|
|
|
logOptionsStorageKey?: string;
|
2025-02-27 18:31:55 +08:00
|
|
|
logs: LogRowModel[];
|
2025-04-07 22:38:55 +08:00
|
|
|
logsMeta?: LogsMetaItem[];
|
2025-02-28 00:34:02 +08:00
|
|
|
logSupportsContext?: (row: LogRowModel) => boolean;
|
2025-07-18 19:13:51 +08:00
|
|
|
noInteractions?: boolean;
|
2025-05-21 01:28:35 +08:00
|
|
|
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
|
|
|
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
|
|
|
onClickFilterString?: (value: string, refId?: string) => void;
|
|
|
|
onClickFilterOutString?: (value: string, refId?: string) => void;
|
|
|
|
onClickShowField?: (key: string) => void;
|
|
|
|
onClickHideField?: (key: string) => void;
|
2025-10-07 19:19:27 +08:00
|
|
|
onLogOptionsChange?: (option: LogListOptions, value: string | boolean | string[]) => void;
|
2025-04-04 20:53:12 +08:00
|
|
|
onLogLineHover?: (row?: LogRowModel) => void;
|
2025-02-28 00:34:02 +08:00
|
|
|
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
|
|
|
onPinLine?: (row: LogRowModel) => void;
|
|
|
|
onOpenContext?: (row: LogRowModel, onClose: () => void) => void;
|
|
|
|
onUnpinLine?: (row: LogRowModel) => void;
|
2025-05-23 02:12:06 +08:00
|
|
|
permalinkedLogId?: string;
|
2025-02-28 00:34:02 +08:00
|
|
|
pinLineButtonTooltipTitle?: PopoverContent;
|
|
|
|
pinnedLogs?: string[];
|
2025-09-17 18:13:13 +08:00
|
|
|
prettifyJSON?: boolean;
|
2025-07-08 17:57:28 +08:00
|
|
|
setDisplayedFields?: (displayedFields: string[]) => void;
|
2025-04-04 20:53:12 +08:00
|
|
|
showControls: boolean;
|
2025-10-07 19:19:27 +08:00
|
|
|
/**
|
|
|
|
* Experimental. When OTel logs are displayed, add an extra displayed field with relevant key-value pairs from labels and metadata
|
|
|
|
* @alpha
|
|
|
|
*/
|
|
|
|
showLogAttributes?: boolean;
|
2025-02-05 01:40:17 +08:00
|
|
|
showTime: boolean;
|
2025-09-17 23:26:42 +08:00
|
|
|
showUniqueLabels?: boolean;
|
2025-02-05 01:40:17 +08:00
|
|
|
sortOrder: LogsSortOrder;
|
2025-02-14 19:52:34 +08:00
|
|
|
timeRange: TimeRange;
|
2025-08-09 01:13:01 +08:00
|
|
|
timestampResolution?: LogLineTimestampResolution;
|
2025-02-05 01:40:17 +08:00
|
|
|
timeZone: string;
|
2025-04-04 20:53:12 +08:00
|
|
|
syntaxHighlighting?: boolean;
|
2025-02-05 01:40:17 +08:00
|
|
|
wrapLogMessage: boolean;
|
|
|
|
}
|
|
|
|
|
2025-06-10 17:59:01 +08:00
|
|
|
export type LogListFontSize = 'default' | 'small';
|
|
|
|
|
2025-10-07 19:19:27 +08:00
|
|
|
export type LogListOptions = keyof LogListState | 'wrapLogMessage' | 'prettifyLogMessage' | 'defaultDisplayedFields';
|
2025-04-04 20:53:12 +08:00
|
|
|
|
|
|
|
type LogListComponentProps = Omit<
|
|
|
|
Props,
|
2025-05-21 01:28:35 +08:00
|
|
|
| 'app'
|
|
|
|
| 'dedupStrategy'
|
|
|
|
| 'displayedFields'
|
|
|
|
| 'enableLogDetails'
|
2025-07-08 17:57:28 +08:00
|
|
|
| 'logOptionsStorageKey'
|
2025-05-23 02:12:06 +08:00
|
|
|
| 'permalinkedLogId'
|
2025-05-21 01:28:35 +08:00
|
|
|
| 'showTime'
|
|
|
|
| 'sortOrder'
|
|
|
|
| 'syntaxHighlighting'
|
|
|
|
| 'wrapLogMessage'
|
2025-04-04 20:53:12 +08:00
|
|
|
>;
|
|
|
|
|
2025-02-05 01:40:17 +08:00
|
|
|
export const LogList = ({
|
|
|
|
app,
|
2025-04-04 20:53:12 +08:00
|
|
|
displayedFields,
|
2025-02-05 01:40:17 +08:00
|
|
|
containerElement,
|
2025-07-09 22:07:19 +08:00
|
|
|
logOptionsStorageKey,
|
2025-09-04 00:53:38 +08:00
|
|
|
detailsMode,
|
2025-04-04 20:53:12 +08:00
|
|
|
dedupStrategy,
|
2025-05-21 01:28:35 +08:00
|
|
|
enableLogDetails,
|
2025-02-05 01:40:17 +08:00
|
|
|
eventBus,
|
2025-04-04 20:53:12 +08:00
|
|
|
filterLevels,
|
2025-06-10 17:59:01 +08:00
|
|
|
fontSize = logOptionsStorageKey ? (store.get(`${logOptionsStorageKey}.fontSize`) ?? 'default') : 'default',
|
2025-02-27 18:31:55 +08:00
|
|
|
getFieldLinks,
|
2025-04-04 20:53:12 +08:00
|
|
|
getRowContextQuery,
|
|
|
|
grammar,
|
2025-08-01 19:30:17 +08:00
|
|
|
infiniteScrollMode,
|
2025-02-14 19:52:34 +08:00
|
|
|
initialScrollPosition = 'top',
|
2025-05-21 01:28:35 +08:00
|
|
|
isLabelFilterActive,
|
2025-05-07 06:26:54 +08:00
|
|
|
loading,
|
2025-02-14 19:52:34 +08:00
|
|
|
loadMore,
|
2025-06-01 20:29:49 +08:00
|
|
|
logLineMenuCustomItems,
|
2025-02-14 19:52:34 +08:00
|
|
|
logs,
|
2025-04-07 22:38:55 +08:00
|
|
|
logsMeta,
|
2025-04-04 20:53:12 +08:00
|
|
|
logSupportsContext,
|
2025-07-18 19:13:51 +08:00
|
|
|
noInteractions,
|
2025-05-21 01:28:35 +08:00
|
|
|
onClickFilterLabel,
|
|
|
|
onClickFilterOutLabel,
|
|
|
|
onClickFilterString,
|
|
|
|
onClickFilterOutString,
|
|
|
|
onClickShowField,
|
|
|
|
onClickHideField,
|
2025-04-04 20:53:12 +08:00
|
|
|
onLogOptionsChange,
|
|
|
|
onLogLineHover,
|
|
|
|
onPermalinkClick,
|
|
|
|
onPinLine,
|
|
|
|
onOpenContext,
|
|
|
|
onUnpinLine,
|
2025-05-23 02:12:06 +08:00
|
|
|
permalinkedLogId,
|
2025-04-04 20:53:12 +08:00
|
|
|
pinLineButtonTooltipTitle,
|
|
|
|
pinnedLogs,
|
2025-09-17 18:13:13 +08:00
|
|
|
prettifyJSON = logOptionsStorageKey ? store.getBool(`${logOptionsStorageKey}.prettifyLogMessage`, true) : true,
|
2025-07-08 17:57:28 +08:00
|
|
|
setDisplayedFields,
|
2025-04-04 20:53:12 +08:00
|
|
|
showControls,
|
2025-10-07 19:19:27 +08:00
|
|
|
showLogAttributes,
|
2025-02-05 01:40:17 +08:00
|
|
|
showTime,
|
2025-09-17 23:26:42 +08:00
|
|
|
showUniqueLabels,
|
2025-02-05 01:40:17 +08:00
|
|
|
sortOrder,
|
2025-04-07 22:38:55 +08:00
|
|
|
syntaxHighlighting = logOptionsStorageKey ? store.getBool(`${logOptionsStorageKey}.syntaxHighlighting`, true) : true,
|
2025-02-14 19:52:34 +08:00
|
|
|
timeRange,
|
2025-08-09 01:13:01 +08:00
|
|
|
timestampResolution,
|
2025-02-05 01:40:17 +08:00
|
|
|
timeZone,
|
|
|
|
wrapLogMessage,
|
|
|
|
}: Props) => {
|
2025-04-04 20:53:12 +08:00
|
|
|
return (
|
|
|
|
<LogListContextProvider
|
|
|
|
app={app}
|
2025-05-21 01:28:35 +08:00
|
|
|
containerElement={containerElement}
|
2025-04-04 20:53:12 +08:00
|
|
|
dedupStrategy={dedupStrategy}
|
2025-07-09 22:07:19 +08:00
|
|
|
detailsMode={detailsMode}
|
2025-04-04 20:53:12 +08:00
|
|
|
displayedFields={displayedFields}
|
2025-05-21 01:28:35 +08:00
|
|
|
enableLogDetails={enableLogDetails}
|
2025-04-04 20:53:12 +08:00
|
|
|
filterLevels={filterLevels}
|
2025-06-10 17:59:01 +08:00
|
|
|
fontSize={fontSize}
|
2025-04-04 20:53:12 +08:00
|
|
|
getRowContextQuery={getRowContextQuery}
|
2025-05-21 01:28:35 +08:00
|
|
|
isLabelFilterActive={isLabelFilterActive}
|
2025-04-07 22:38:55 +08:00
|
|
|
logs={logs}
|
|
|
|
logsMeta={logsMeta}
|
2025-06-01 20:29:49 +08:00
|
|
|
logLineMenuCustomItems={logLineMenuCustomItems}
|
2025-04-04 20:53:12 +08:00
|
|
|
logOptionsStorageKey={logOptionsStorageKey}
|
|
|
|
logSupportsContext={logSupportsContext}
|
2025-07-18 19:13:51 +08:00
|
|
|
noInteractions={noInteractions}
|
2025-05-21 01:28:35 +08:00
|
|
|
onClickFilterLabel={onClickFilterLabel}
|
|
|
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
|
|
|
onClickFilterString={onClickFilterString}
|
|
|
|
onClickFilterOutString={onClickFilterOutString}
|
|
|
|
onClickShowField={onClickShowField}
|
|
|
|
onClickHideField={onClickHideField}
|
2025-04-04 20:53:12 +08:00
|
|
|
onLogOptionsChange={onLogOptionsChange}
|
|
|
|
onLogLineHover={onLogLineHover}
|
|
|
|
onPermalinkClick={onPermalinkClick}
|
|
|
|
onPinLine={onPinLine}
|
|
|
|
onOpenContext={onOpenContext}
|
|
|
|
onUnpinLine={onUnpinLine}
|
2025-05-23 02:12:06 +08:00
|
|
|
permalinkedLogId={permalinkedLogId}
|
2025-04-04 20:53:12 +08:00
|
|
|
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
|
|
|
pinnedLogs={pinnedLogs}
|
2025-09-17 18:13:13 +08:00
|
|
|
prettifyJSON={prettifyJSON}
|
2025-07-08 17:57:28 +08:00
|
|
|
setDisplayedFields={setDisplayedFields}
|
2025-04-04 20:53:12 +08:00
|
|
|
showControls={showControls}
|
2025-10-07 19:19:27 +08:00
|
|
|
showLogAttributes={showLogAttributes}
|
2025-04-04 20:53:12 +08:00
|
|
|
showTime={showTime}
|
2025-09-17 23:26:42 +08:00
|
|
|
showUniqueLabels={showUniqueLabels}
|
2025-04-04 20:53:12 +08:00
|
|
|
sortOrder={sortOrder}
|
|
|
|
syntaxHighlighting={syntaxHighlighting}
|
2025-08-09 01:13:01 +08:00
|
|
|
timestampResolution={timestampResolution}
|
2025-04-04 20:53:12 +08:00
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
>
|
2025-06-13 20:50:03 +08:00
|
|
|
<LogListSearchContextProvider>
|
|
|
|
<LogListComponent
|
|
|
|
containerElement={containerElement}
|
|
|
|
eventBus={eventBus}
|
|
|
|
getFieldLinks={getFieldLinks}
|
|
|
|
grammar={grammar}
|
|
|
|
initialScrollPosition={initialScrollPosition}
|
2025-08-01 19:30:17 +08:00
|
|
|
infiniteScrollMode={infiniteScrollMode}
|
2025-08-21 21:50:05 +08:00
|
|
|
loading={loading}
|
2025-06-13 20:50:03 +08:00
|
|
|
loadMore={loadMore}
|
|
|
|
logs={logs}
|
|
|
|
showControls={showControls}
|
|
|
|
timeRange={timeRange}
|
|
|
|
timeZone={timeZone}
|
|
|
|
/>
|
|
|
|
</LogListSearchContextProvider>
|
2025-04-04 20:53:12 +08:00
|
|
|
</LogListContextProvider>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const LogListComponent = ({
|
|
|
|
containerElement,
|
|
|
|
eventBus = new EventBusSrv(),
|
|
|
|
getFieldLinks,
|
|
|
|
grammar,
|
|
|
|
initialScrollPosition = 'top',
|
2025-08-01 19:30:17 +08:00
|
|
|
infiniteScrollMode = 'interval',
|
2025-08-21 21:50:05 +08:00
|
|
|
loading,
|
2025-04-04 20:53:12 +08:00
|
|
|
loadMore,
|
|
|
|
logs,
|
|
|
|
showControls,
|
|
|
|
timeRange,
|
|
|
|
timeZone,
|
|
|
|
}: LogListComponentProps) => {
|
2025-05-21 01:28:35 +08:00
|
|
|
const {
|
|
|
|
app,
|
|
|
|
displayedFields,
|
|
|
|
dedupStrategy,
|
2025-07-09 22:07:19 +08:00
|
|
|
detailsMode,
|
2025-05-21 01:28:35 +08:00
|
|
|
filterLevels,
|
2025-06-10 17:59:01 +08:00
|
|
|
fontSize,
|
2025-05-21 01:28:35 +08:00
|
|
|
forceEscape,
|
2025-06-01 20:29:49 +08:00
|
|
|
hasLogsWithErrors,
|
|
|
|
hasSampledLogs,
|
2025-06-10 17:59:01 +08:00
|
|
|
onClickFilterString,
|
|
|
|
onClickFilterOutString,
|
2025-05-23 02:12:06 +08:00
|
|
|
permalinkedLogId,
|
2025-08-28 01:34:14 +08:00
|
|
|
prettifyJSON,
|
2025-05-21 01:28:35 +08:00
|
|
|
showDetails,
|
|
|
|
showTime,
|
2025-09-17 23:26:42 +08:00
|
|
|
showUniqueLabels,
|
2025-05-21 01:28:35 +08:00
|
|
|
sortOrder,
|
2025-08-09 01:13:01 +08:00
|
|
|
timestampResolution,
|
2025-05-21 01:28:35 +08:00
|
|
|
toggleDetails,
|
|
|
|
wrapLogMessage,
|
|
|
|
} = useLogListContext();
|
2025-02-14 19:52:34 +08:00
|
|
|
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
|
2025-08-05 21:03:03 +08:00
|
|
|
const [listHeight, setListHeight] = useState(getListHeight(containerElement, app));
|
2025-02-05 01:40:17 +08:00
|
|
|
const theme = useTheme2();
|
|
|
|
const listRef = useRef<VariableSizeList | null>(null);
|
|
|
|
const widthRef = useRef(containerElement.clientWidth);
|
2025-05-21 01:28:35 +08:00
|
|
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
2025-02-14 19:52:34 +08:00
|
|
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
2025-06-21 20:38:46 +08:00
|
|
|
const virtualization = useMemo(() => new LogLineVirtualization(theme, fontSize), [theme, fontSize]);
|
2025-02-27 18:31:55 +08:00
|
|
|
const dimensions = useMemo(
|
2025-08-09 01:13:01 +08:00
|
|
|
() =>
|
|
|
|
wrapLogMessage
|
|
|
|
? []
|
2025-09-17 23:26:42 +08:00
|
|
|
: virtualization.calculateFieldDimensions(
|
|
|
|
processedLogs,
|
|
|
|
displayedFields,
|
|
|
|
timestampResolution,
|
|
|
|
showUniqueLabels
|
|
|
|
),
|
|
|
|
[displayedFields, processedLogs, showUniqueLabels, timestampResolution, virtualization, wrapLogMessage]
|
2025-02-27 18:31:55 +08:00
|
|
|
);
|
2025-07-01 17:55:26 +08:00
|
|
|
const styles = useStyles2(getStyles, dimensions, displayedFields, { showTime });
|
2025-05-21 01:28:35 +08:00
|
|
|
const widthContainer = wrapperRef.current ?? containerElement;
|
2025-06-10 17:59:01 +08:00
|
|
|
const {
|
|
|
|
closePopoverMenu,
|
|
|
|
handleTextSelection,
|
|
|
|
onDisableCancel,
|
|
|
|
onDisableConfirm,
|
|
|
|
onDisablePopoverMenu,
|
|
|
|
popoverState,
|
|
|
|
showDisablePopoverOptions,
|
|
|
|
} = usePopoverMenu(wrapperRef.current);
|
2025-06-13 20:50:03 +08:00
|
|
|
useKeyBindings();
|
|
|
|
const { filterLogs, matchingUids, searchVisible } = useLogListSearchContext();
|
2025-05-21 01:28:35 +08:00
|
|
|
|
2025-08-01 19:30:17 +08:00
|
|
|
const levelFilteredLogs = useMemo(
|
|
|
|
() =>
|
|
|
|
filterLevels.length === 0 ? processedLogs : processedLogs.filter((log) => filterLevels.includes(log.logLevel)),
|
|
|
|
[filterLevels, processedLogs]
|
|
|
|
);
|
|
|
|
|
|
|
|
const filteredLogs = useMemo(
|
|
|
|
() =>
|
|
|
|
matchingUids && filterLogs
|
|
|
|
? levelFilteredLogs.filter((log) => matchingUids.includes(log.uid))
|
|
|
|
: levelFilteredLogs,
|
|
|
|
[filterLogs, levelFilteredLogs, matchingUids]
|
|
|
|
);
|
|
|
|
|
2025-09-17 23:26:42 +08:00
|
|
|
// When log lines report size discrepancies, we debounce the calculation reset to give time to
|
|
|
|
// use the smallest log index to reset the heights.
|
2025-05-21 01:28:35 +08:00
|
|
|
const debouncedResetAfterIndex = useMemo(() => {
|
|
|
|
return debounce((index: number) => {
|
|
|
|
listRef.current?.resetAfterIndex(index);
|
|
|
|
overflowIndexRef.current = Infinity;
|
2025-09-17 23:26:42 +08:00
|
|
|
}, 0);
|
2025-05-21 01:28:35 +08:00
|
|
|
}, []);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
2025-07-08 17:57:28 +08:00
|
|
|
const debouncedScrollToItem = useMemo(() => {
|
|
|
|
return debounce((index: number, align?: Align) => {
|
|
|
|
listRef.current?.scrollToItem(index, align);
|
|
|
|
}, 250);
|
|
|
|
}, []);
|
|
|
|
|
2025-02-05 01:40:17 +08:00
|
|
|
useEffect(() => {
|
2025-02-14 19:52:34 +08:00
|
|
|
const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) =>
|
2025-08-01 19:30:17 +08:00
|
|
|
handleScrollToEvent(e, filteredLogs, listRef.current)
|
2025-02-14 19:52:34 +08:00
|
|
|
);
|
2025-02-05 01:40:17 +08:00
|
|
|
return () => subscription.unsubscribe();
|
2025-08-01 19:30:17 +08:00
|
|
|
}, [eventBus, filteredLogs]);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
2025-04-11 02:47:17 +08:00
|
|
|
setProcessedLogs(
|
2025-06-21 20:38:46 +08:00
|
|
|
preProcessLogs(
|
|
|
|
logs,
|
2025-08-28 01:34:14 +08:00
|
|
|
{
|
|
|
|
getFieldLinks,
|
|
|
|
escape: forceEscape ?? false,
|
|
|
|
prettifyJSON,
|
|
|
|
order: sortOrder,
|
|
|
|
timeZone,
|
|
|
|
virtualization,
|
|
|
|
wrapLogMessage,
|
|
|
|
},
|
2025-06-21 20:38:46 +08:00
|
|
|
grammar
|
|
|
|
)
|
2025-04-11 02:47:17 +08:00
|
|
|
);
|
2025-06-21 20:38:46 +08:00
|
|
|
virtualization.resetLogLineSizes();
|
2025-05-07 06:26:54 +08:00
|
|
|
listRef.current?.resetAfterIndex(0);
|
2025-08-28 01:34:14 +08:00
|
|
|
}, [forceEscape, getFieldLinks, grammar, logs, prettifyJSON, sortOrder, timeZone, virtualization, wrapLogMessage]);
|
2025-03-26 19:48:26 +08:00
|
|
|
|
|
|
|
useEffect(() => {
|
2025-02-05 01:40:17 +08:00
|
|
|
listRef.current?.resetAfterIndex(0);
|
2025-06-01 20:29:49 +08:00
|
|
|
}, [wrapLogMessage, showDetails, displayedFields, dedupStrategy]);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
2025-09-17 23:26:42 +08:00
|
|
|
const handleResize = (entry: ResizeObserverEntry) => {
|
2025-08-05 21:03:03 +08:00
|
|
|
setListHeight(getListHeight(containerElement, app, searchVisible));
|
2025-09-17 23:26:42 +08:00
|
|
|
if (widthRef.current !== entry.contentRect.width) {
|
|
|
|
widthRef.current = entry.contentRect.width;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
|
|
|
if (entries.length) {
|
|
|
|
handleResize(entries[0]);
|
|
|
|
}
|
|
|
|
});
|
2025-08-05 21:03:03 +08:00
|
|
|
observer.observe(containerElement);
|
|
|
|
return () => observer.disconnect();
|
|
|
|
}, [app, containerElement, searchVisible]);
|
|
|
|
|
2025-05-21 01:28:35 +08:00
|
|
|
const overflowIndexRef = useRef(Infinity);
|
2025-02-05 01:40:17 +08:00
|
|
|
const handleOverflow = useCallback(
|
2025-05-07 06:26:54 +08:00
|
|
|
(index: number, id: string, height?: number) => {
|
2025-05-21 01:28:35 +08:00
|
|
|
if (height !== undefined) {
|
2025-06-21 20:38:46 +08:00
|
|
|
virtualization.storeLogLineSize(id, widthContainer, height);
|
2025-06-10 17:59:01 +08:00
|
|
|
}
|
|
|
|
if (index === overflowIndexRef.current) {
|
|
|
|
return;
|
2025-02-05 01:40:17 +08:00
|
|
|
}
|
2025-05-21 01:28:35 +08:00
|
|
|
overflowIndexRef.current = index < overflowIndexRef.current ? index : overflowIndexRef.current;
|
|
|
|
debouncedResetAfterIndex(overflowIndexRef.current);
|
2025-02-05 01:40:17 +08:00
|
|
|
},
|
2025-06-21 20:38:46 +08:00
|
|
|
[debouncedResetAfterIndex, virtualization, widthContainer]
|
2025-02-05 01:40:17 +08:00
|
|
|
);
|
|
|
|
|
2025-08-05 21:03:03 +08:00
|
|
|
const handleScrollPosition = useCallback(
|
|
|
|
(log?: LogListModel) => {
|
|
|
|
const scrollToUID = log ? log.uid : permalinkedLogId;
|
|
|
|
if (scrollToUID) {
|
|
|
|
const index = processedLogs.findIndex((log) => log.uid === scrollToUID);
|
|
|
|
if (index >= 0) {
|
|
|
|
listRef.current?.scrollToItem(index, 'start');
|
|
|
|
return;
|
|
|
|
}
|
2025-05-23 02:12:06 +08:00
|
|
|
}
|
2025-08-05 21:03:03 +08:00
|
|
|
listRef.current?.scrollToItem(initialScrollPosition === 'top' ? 0 : processedLogs.length - 1);
|
|
|
|
},
|
|
|
|
[initialScrollPosition, permalinkedLogId, processedLogs]
|
|
|
|
);
|
2025-02-05 01:40:17 +08:00
|
|
|
|
2025-05-21 01:28:35 +08:00
|
|
|
const handleLogLineClick = useCallback(
|
2025-06-10 17:59:01 +08:00
|
|
|
(e: MouseEvent<HTMLElement>, log: LogListModel) => {
|
|
|
|
if (handleTextSelection(e, log)) {
|
|
|
|
// Event handled by the parent.
|
2025-05-23 02:12:06 +08:00
|
|
|
return;
|
|
|
|
}
|
2025-05-21 01:28:35 +08:00
|
|
|
toggleDetails(log);
|
|
|
|
},
|
2025-06-10 17:59:01 +08:00
|
|
|
[handleTextSelection, toggleDetails]
|
2025-05-21 01:28:35 +08:00
|
|
|
);
|
|
|
|
|
2025-07-08 17:57:28 +08:00
|
|
|
const focusLogLine = useCallback(
|
|
|
|
(log: LogListModel) => {
|
2025-08-30 02:33:21 +08:00
|
|
|
const index = filteredLogs.findIndex((filteredLog) => filteredLog.uid === log.uid);
|
2025-07-08 17:57:28 +08:00
|
|
|
if (index >= 0) {
|
|
|
|
debouncedScrollToItem(index, 'start');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[debouncedScrollToItem, filteredLogs]
|
|
|
|
);
|
|
|
|
|
2025-09-19 17:29:26 +08:00
|
|
|
const logLevels = useMemo(() => getLevelsFromLogs(processedLogs), [processedLogs]);
|
|
|
|
|
2025-08-25 23:50:49 +08:00
|
|
|
if (!containerElement || listHeight == null) {
|
|
|
|
// Wait for container to be rendered
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2025-02-05 01:40:17 +08:00
|
|
|
return (
|
2025-04-04 20:53:12 +08:00
|
|
|
<div className={styles.logListContainer}>
|
2025-09-19 17:29:26 +08:00
|
|
|
{showControls && <LogListControls logLevels={logLevels} eventBus={eventBus} />}
|
2025-08-01 19:30:17 +08:00
|
|
|
{detailsMode === 'sidebar' && showDetails.length > 0 && (
|
|
|
|
<LogLineDetails
|
|
|
|
containerElement={containerElement}
|
|
|
|
focusLogLine={focusLogLine}
|
|
|
|
logs={filteredLogs}
|
2025-08-25 22:06:01 +08:00
|
|
|
timeRange={timeRange}
|
|
|
|
timeZone={timeZone}
|
2025-09-03 23:44:29 +08:00
|
|
|
showControls={showControls}
|
2025-08-01 19:30:17 +08:00
|
|
|
/>
|
|
|
|
)}
|
2025-05-21 01:28:35 +08:00
|
|
|
<div className={styles.logListWrapper} ref={wrapperRef}>
|
2025-06-10 17:59:01 +08:00
|
|
|
{popoverState.selection && popoverState.selectedRow && (
|
|
|
|
<PopoverMenu
|
|
|
|
close={closePopoverMenu}
|
|
|
|
row={popoverState.selectedRow}
|
|
|
|
selection={popoverState.selection}
|
|
|
|
{...popoverState.popoverMenuCoordinates}
|
|
|
|
onClickFilterString={onClickFilterString}
|
|
|
|
onClickFilterOutString={onClickFilterOutString}
|
|
|
|
onDisable={onDisablePopoverMenu}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{showDisablePopoverOptions && (
|
|
|
|
<ConfirmModal
|
|
|
|
isOpen
|
|
|
|
title={t('logs.log-rows.disable-popover.title', 'Disable menu')}
|
|
|
|
body={
|
|
|
|
<>
|
|
|
|
<Trans i18nKey="logs.log-rows.disable-popover.message">
|
|
|
|
You are about to disable the logs filter menu. To re-enable it, select text in a log line while
|
|
|
|
holding the alt key.
|
|
|
|
</Trans>
|
|
|
|
<div className={styles.shortcut}>
|
|
|
|
<Icon name="keyboard" />
|
|
|
|
<Trans i18nKey="logs.log-rows.disable-popover-message.shortcut">alt+select to enable again</Trans>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
confirmText={t('logs.log-rows.disable-popover.confirm', 'Confirm')}
|
|
|
|
icon="exclamation-triangle"
|
|
|
|
onConfirm={onDisableConfirm}
|
|
|
|
onDismiss={onDisableCancel}
|
|
|
|
/>
|
|
|
|
)}
|
2025-06-13 20:50:03 +08:00
|
|
|
<LogListSearch logs={levelFilteredLogs} listRef={listRef.current} />
|
2025-05-21 01:28:35 +08:00
|
|
|
<InfiniteScroll
|
|
|
|
displayedFields={displayedFields}
|
|
|
|
handleOverflow={handleOverflow}
|
2025-08-01 19:30:17 +08:00
|
|
|
infiniteScrollMode={infiniteScrollMode}
|
2025-08-21 21:50:05 +08:00
|
|
|
loading={loading}
|
2025-05-21 01:28:35 +08:00
|
|
|
logs={filteredLogs}
|
|
|
|
loadMore={loadMore}
|
|
|
|
onClick={handleLogLineClick}
|
|
|
|
scrollElement={scrollRef.current}
|
|
|
|
showTime={showTime}
|
|
|
|
sortOrder={sortOrder}
|
|
|
|
timeRange={timeRange}
|
|
|
|
timeZone={timeZone}
|
|
|
|
setInitialScrollPosition={handleScrollPosition}
|
2025-06-21 20:38:46 +08:00
|
|
|
virtualization={virtualization}
|
2025-05-21 01:28:35 +08:00
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
>
|
|
|
|
{({ getItemKey, itemCount, onItemsRendered, Renderer }) => (
|
|
|
|
<VariableSizeList
|
|
|
|
className={styles.logList}
|
|
|
|
height={listHeight}
|
|
|
|
itemCount={itemCount}
|
2025-06-21 20:38:46 +08:00
|
|
|
itemSize={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, {
|
2025-07-09 22:07:19 +08:00
|
|
|
detailsMode,
|
2025-06-01 20:29:49 +08:00
|
|
|
hasLogsWithErrors,
|
|
|
|
hasSampledLogs,
|
2025-05-21 01:28:35 +08:00
|
|
|
showDuplicates: dedupStrategy !== LogsDedupStrategy.none,
|
2025-07-09 22:07:19 +08:00
|
|
|
showDetails,
|
2025-05-21 01:28:35 +08:00
|
|
|
showTime,
|
|
|
|
wrap: wrapLogMessage,
|
|
|
|
})}
|
|
|
|
itemKey={getItemKey}
|
|
|
|
layout="vertical"
|
|
|
|
onItemsRendered={onItemsRendered}
|
|
|
|
outerRef={scrollRef}
|
2025-06-01 20:29:49 +08:00
|
|
|
overscanCount={5}
|
2025-05-21 01:28:35 +08:00
|
|
|
ref={listRef}
|
2025-07-23 23:10:22 +08:00
|
|
|
style={wrapLogMessage ? { overflowY: 'scroll' } : { overflow: 'scroll' }}
|
2025-05-21 01:28:35 +08:00
|
|
|
width="100%"
|
|
|
|
>
|
|
|
|
{Renderer}
|
|
|
|
</VariableSizeList>
|
|
|
|
)}
|
|
|
|
</InfiniteScroll>
|
|
|
|
</div>
|
2025-04-04 20:53:12 +08:00
|
|
|
</div>
|
2025-02-05 01:40:17 +08:00
|
|
|
);
|
|
|
|
};
|
2025-02-14 19:52:34 +08:00
|
|
|
|
2025-07-01 17:55:26 +08:00
|
|
|
function getStyles(
|
|
|
|
theme: GrafanaTheme2,
|
|
|
|
dimensions: LogFieldDimension[],
|
|
|
|
displayedFields: string[],
|
|
|
|
{ showTime }: { showTime: boolean }
|
|
|
|
) {
|
2025-02-27 18:31:55 +08:00
|
|
|
const columns = showTime ? dimensions : dimensions.filter((_, index) => index > 0);
|
|
|
|
return {
|
|
|
|
logList: css({
|
|
|
|
'& .unwrapped-log-line': {
|
|
|
|
display: 'grid',
|
2025-07-01 17:55:26 +08:00
|
|
|
gridTemplateColumns: getGridTemplateColumns(columns, displayedFields),
|
2025-07-09 22:07:19 +08:00
|
|
|
'& .field': {
|
|
|
|
overflow: 'hidden',
|
|
|
|
},
|
2025-02-27 18:31:55 +08:00
|
|
|
},
|
|
|
|
}),
|
2025-04-04 20:53:12 +08:00
|
|
|
logListContainer: css({
|
|
|
|
display: 'flex',
|
2025-08-01 19:30:17 +08:00
|
|
|
flexDirection: 'row-reverse',
|
2025-06-10 17:59:01 +08:00
|
|
|
// Minimum width to prevent rendering issues and a sausage-like logs panel.
|
|
|
|
minWidth: theme.spacing(35),
|
2025-04-04 20:53:12 +08:00
|
|
|
}),
|
2025-05-21 01:28:35 +08:00
|
|
|
logListWrapper: css({
|
2025-06-10 17:59:01 +08:00
|
|
|
position: 'relative',
|
2025-06-13 20:50:03 +08:00
|
|
|
width: '100%',
|
2025-06-10 17:59:01 +08:00
|
|
|
}),
|
|
|
|
shortcut: css({
|
|
|
|
display: 'inline-flex',
|
|
|
|
alignItems: 'center',
|
|
|
|
gap: theme.spacing(1),
|
|
|
|
color: theme.colors.text.secondary,
|
|
|
|
opacity: 0.7,
|
|
|
|
fontSize: theme.typography.bodySmall.fontSize,
|
|
|
|
marginTop: theme.spacing(1),
|
2025-05-21 01:28:35 +08:00
|
|
|
}),
|
2025-02-27 18:31:55 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2025-08-01 19:30:17 +08:00
|
|
|
function handleScrollToEvent(event: ScrollToLogsEvent, logs: LogListModel[], list: VariableSizeList | null) {
|
2025-02-14 19:52:34 +08:00
|
|
|
if (event.payload.scrollTo === 'top') {
|
|
|
|
list?.scrollTo(0);
|
2025-08-01 19:30:17 +08:00
|
|
|
} else if (event.payload.scrollTo === 'bottom') {
|
|
|
|
list?.scrollToItem(logs.length - 1);
|
2025-02-14 19:52:34 +08:00
|
|
|
} else {
|
2025-08-01 19:30:17 +08:00
|
|
|
// uid
|
|
|
|
const index = logs.findIndex((log) => log.uid === event.payload.scrollTo);
|
|
|
|
if (index >= 0) {
|
|
|
|
list?.scrollToItem(index, 'center');
|
|
|
|
}
|
2025-02-14 19:52:34 +08:00
|
|
|
}
|
|
|
|
}
|
2025-08-05 21:03:03 +08:00
|
|
|
|
|
|
|
function getListHeight(containerElement: HTMLDivElement, app: CoreApp, searchVisible = false) {
|
|
|
|
return (
|
|
|
|
(app === CoreApp.Explore
|
|
|
|
? Math.max(window.innerHeight * 0.8, containerElement.clientHeight)
|
|
|
|
: containerElement.clientHeight) - (searchVisible ? LOG_LIST_SEARCH_HEIGHT : 0)
|
|
|
|
);
|
|
|
|
}
|