2023-04-14 23:05:43 +08:00
|
|
|
import { debounce } from 'lodash';
|
2024-12-19 02:03:47 +08:00
|
|
|
import { MouseEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2025-04-15 22:08:29 +08:00
|
|
|
import { CoreApp, DataFrame, dateTimeFormat, LogRowContextOptions, LogRowModel, LogsSortOrder } from '@grafana/data';
|
2025-05-15 15:17:14 +08:00
|
|
|
import { useTranslate } from '@grafana/i18n';
|
2022-10-11 17:04:43 +08:00
|
|
|
import { reportInteraction } from '@grafana/runtime';
|
2023-12-21 05:10:29 +08:00
|
|
|
import { DataQuery, TimeZone } from '@grafana/schema';
|
2024-12-19 02:03:47 +08:00
|
|
|
import { Icon, PopoverContent, Tooltip, useTheme2 } from '@grafana/ui';
|
2025-04-15 22:08:29 +08:00
|
|
|
import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2024-07-12 21:14:53 +08:00
|
|
|
import { checkLogsError, checkLogsSampled, escapeUnescapedString } from '../utils';
|
2022-09-30 18:16:47 +08:00
|
|
|
|
2022-09-19 16:51:46 +08:00
|
|
|
import { LogDetails } from './LogDetails';
|
|
|
|
import { LogLabels } from './LogLabels';
|
2023-04-11 18:19:28 +08:00
|
|
|
import { LogRowMessage } from './LogRowMessage';
|
|
|
|
import { LogRowMessageDisplayedFields } from './LogRowMessageDisplayedFields';
|
|
|
|
import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles';
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
export interface Props {
|
2022-09-19 16:51:46 +08:00
|
|
|
row: LogRowModel;
|
|
|
|
showDuplicates: boolean;
|
|
|
|
showLabels: boolean;
|
|
|
|
showTime: boolean;
|
|
|
|
wrapLogMessage: boolean;
|
|
|
|
prettifyLogMessage: boolean;
|
|
|
|
timeZone: TimeZone;
|
|
|
|
enableLogDetails: boolean;
|
|
|
|
logsSortOrder?: LogsSortOrder | null;
|
|
|
|
forceEscape?: boolean;
|
2022-09-29 16:00:01 +08:00
|
|
|
app?: CoreApp;
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields?: string[];
|
2022-09-19 16:51:46 +08:00
|
|
|
getRows: () => LogRowModel[];
|
2023-11-27 21:29:00 +08:00
|
|
|
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
|
|
|
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
2022-09-19 16:51:46 +08:00
|
|
|
onContextClick?: () => void;
|
2025-04-15 22:08:29 +08:00
|
|
|
getFieldLinks?: GetFieldLinksFn;
|
2023-10-31 19:37:44 +08:00
|
|
|
showContextToggle?: (row: LogRowModel) => boolean;
|
2023-01-12 02:20:11 +08:00
|
|
|
onClickShowField?: (key: string) => void;
|
|
|
|
onClickHideField?: (key: string) => void;
|
2022-09-19 16:51:46 +08:00
|
|
|
onLogRowHover?: (row?: LogRowModel) => void;
|
2023-04-14 23:05:43 +08:00
|
|
|
onOpenContext: (row: LogRowModel, onClose: () => void) => void;
|
2023-12-21 05:10:29 +08:00
|
|
|
getRowContextQuery?: (
|
|
|
|
row: LogRowModel,
|
|
|
|
options?: LogRowContextOptions,
|
2023-12-22 00:02:29 +08:00
|
|
|
cacheFilters?: boolean
|
2023-12-21 05:10:29 +08:00
|
|
|
) => Promise<DataQuery | null>;
|
2023-06-16 20:07:51 +08:00
|
|
|
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
2023-02-01 22:28:10 +08:00
|
|
|
styles: LogRowStyles;
|
2023-06-16 20:07:51 +08:00
|
|
|
permalinkedRowId?: string;
|
|
|
|
scrollIntoView?: (element: HTMLElement) => void;
|
2023-09-04 22:30:17 +08:00
|
|
|
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
2024-07-12 21:14:53 +08:00
|
|
|
onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void;
|
2023-06-28 21:22:54 +08:00
|
|
|
onUnpinLine?: (row: LogRowModel) => void;
|
2024-06-12 01:15:36 +08:00
|
|
|
pinLineButtonTooltipTitle?: PopoverContent;
|
2023-06-28 21:22:54 +08:00
|
|
|
pinned?: boolean;
|
2023-11-16 17:48:10 +08:00
|
|
|
handleTextSelection?: (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel) => boolean;
|
2024-10-29 00:39:12 +08:00
|
|
|
logRowMenuIconsBefore?: ReactNode[];
|
|
|
|
logRowMenuIconsAfter?: ReactNode[];
|
2022-09-19 16:51:46 +08:00
|
|
|
}
|
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
export const LogRow = ({
|
|
|
|
getRows,
|
|
|
|
onClickFilterLabel,
|
|
|
|
onClickFilterOutLabel,
|
|
|
|
onClickShowField,
|
|
|
|
onClickHideField,
|
|
|
|
enableLogDetails,
|
|
|
|
row,
|
|
|
|
showDuplicates,
|
|
|
|
showContextToggle,
|
|
|
|
showLabels,
|
|
|
|
showTime,
|
|
|
|
displayedFields,
|
|
|
|
wrapLogMessage,
|
|
|
|
prettifyLogMessage,
|
|
|
|
getFieldLinks,
|
|
|
|
forceEscape,
|
|
|
|
app,
|
|
|
|
styles,
|
|
|
|
getRowContextQuery,
|
|
|
|
pinned,
|
|
|
|
logRowMenuIconsBefore,
|
|
|
|
logRowMenuIconsAfter,
|
|
|
|
timeZone,
|
|
|
|
permalinkedRowId,
|
|
|
|
scrollIntoView,
|
|
|
|
handleTextSelection,
|
|
|
|
onLogRowHover,
|
|
|
|
...props
|
|
|
|
}: Props) => {
|
|
|
|
const [showingContext, setShowingContext] = useState(false);
|
|
|
|
const [showDetails, setShowDetails] = useState(false);
|
|
|
|
const [mouseIsOver, setMouseIsOver] = useState(false);
|
|
|
|
const [permalinked, setPermalinked] = useState(false);
|
|
|
|
const logLineRef = useRef<HTMLTableRowElement | null>(null);
|
|
|
|
const theme = useTheme2();
|
2022-10-11 17:04:43 +08:00
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
const timestamp = useMemo(
|
|
|
|
() =>
|
|
|
|
dateTimeFormat(row.timeEpochMs, {
|
|
|
|
timeZone: timeZone,
|
|
|
|
defaultWithMS: true,
|
|
|
|
}),
|
|
|
|
[row.timeEpochMs, timeZone]
|
|
|
|
);
|
|
|
|
const levelStyles = useMemo(() => getLogLevelStyles(theme, row.logLevel), [row.logLevel, theme]);
|
|
|
|
const processedRow = useMemo(
|
2025-05-14 21:06:19 +08:00
|
|
|
() => (row.hasUnescapedContent && forceEscape ? { ...row, entry: escapeUnescapedString(row.entry) } : row),
|
2024-12-19 02:03:47 +08:00
|
|
|
[forceEscape, row]
|
|
|
|
);
|
|
|
|
const errorMessage = checkLogsError(row);
|
|
|
|
const hasError = errorMessage !== undefined;
|
|
|
|
const sampleMessage = checkLogsSampled(row);
|
|
|
|
const isSampled = sampleMessage !== undefined;
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
useEffect(() => {
|
|
|
|
if (permalinkedRowId !== row.uid) {
|
|
|
|
setPermalinked(false);
|
2023-11-16 17:48:10 +08:00
|
|
|
return;
|
|
|
|
}
|
2024-12-19 02:03:47 +08:00
|
|
|
if (!permalinked) {
|
|
|
|
setPermalinked(true);
|
2023-06-16 20:07:51 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
if (logLineRef.current && scrollIntoView) {
|
2023-07-26 00:00:10 +08:00
|
|
|
// at this point this row is the permalinked row, so we need to scroll to it and highlight it if possible.
|
2024-12-19 02:03:47 +08:00
|
|
|
scrollIntoView(logLineRef.current);
|
2023-06-20 20:55:51 +08:00
|
|
|
reportInteraction('grafana_explore_logs_permalink_opened', {
|
|
|
|
datasourceType: row.datasourceType ?? 'unknown',
|
|
|
|
logRowUid: row.uid,
|
|
|
|
});
|
2024-12-19 02:03:47 +08:00
|
|
|
setPermalinked(true);
|
2023-06-16 20:07:51 +08:00
|
|
|
}
|
2024-12-19 02:03:47 +08:00
|
|
|
}, [permalinked, permalinkedRowId, row.datasourceType, row.uid, scrollIntoView]);
|
2023-06-16 20:07:51 +08:00
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
// we are debouncing the state change by 3 seconds to highlight the logline after the context closed.
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
const debouncedContextClose = useCallback(
|
|
|
|
debounce(() => {
|
|
|
|
setShowingContext(false);
|
|
|
|
}, 3000),
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
|
|
|
|
const onOpenContext = useCallback(
|
|
|
|
(row: LogRowModel) => {
|
|
|
|
setShowingContext(true);
|
|
|
|
props.onOpenContext(row, debouncedContextClose);
|
|
|
|
},
|
|
|
|
[debouncedContextClose, props]
|
|
|
|
);
|
|
|
|
|
|
|
|
const onRowClick = useCallback(
|
|
|
|
(e: MouseEvent<HTMLTableRowElement>) => {
|
|
|
|
if (handleTextSelection?.(e, row)) {
|
|
|
|
// Event handled by the parent.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!enableLogDetails) {
|
|
|
|
return;
|
|
|
|
}
|
2023-11-06 22:59:48 +08:00
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
setShowDetails((showDetails: boolean) => !showDetails);
|
|
|
|
},
|
|
|
|
[enableLogDetails, handleTextSelection, row]
|
|
|
|
);
|
2024-07-12 21:14:53 +08:00
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
const onMouseEnter = useCallback(() => {
|
|
|
|
setMouseIsOver(true);
|
|
|
|
if (onLogRowHover) {
|
|
|
|
onLogRowHover(row);
|
|
|
|
}
|
|
|
|
}, [onLogRowHover, row]);
|
|
|
|
|
|
|
|
const onMouseMove = useCallback(
|
|
|
|
(e: MouseEvent) => {
|
|
|
|
// No need to worry about text selection.
|
|
|
|
if (!handleTextSelection) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// The user is selecting text, so hide the log row menu so it doesn't interfere.
|
|
|
|
if (document.getSelection()?.toString() && e.buttons > 0) {
|
|
|
|
setMouseIsOver(false);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[handleTextSelection]
|
|
|
|
);
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
const onMouseLeave = useCallback(() => {
|
|
|
|
setMouseIsOver(false);
|
|
|
|
}, []);
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2025-05-15 15:17:14 +08:00
|
|
|
const { t } = useTranslate();
|
|
|
|
|
2024-12-19 02:03:47 +08:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<tr
|
|
|
|
ref={logLineRef}
|
|
|
|
className={`${styles.logsRow} ${hasError ? styles.errorLogRow : ''} ${showingContext || permalinked || pinned ? styles.highlightBackground : ''}`}
|
|
|
|
onClick={onRowClick}
|
|
|
|
onMouseEnter={onMouseEnter}
|
|
|
|
onMouseLeave={onMouseLeave}
|
|
|
|
onMouseMove={onMouseMove}
|
|
|
|
/**
|
|
|
|
* For better accessibility support, we listen to the onFocus event here (to display the LogRowMenuCell), and
|
|
|
|
* to onBlur event in the LogRowMenuCell (to hide it). This way, the LogRowMenuCell is displayed when the user navigates
|
|
|
|
* using the keyboard.
|
|
|
|
*/
|
|
|
|
onFocus={onMouseEnter}
|
|
|
|
>
|
|
|
|
{showDuplicates && (
|
|
|
|
<td className={styles.logsRowDuplicates}>
|
|
|
|
{processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null}
|
|
|
|
</td>
|
|
|
|
)}
|
|
|
|
<td
|
|
|
|
className={
|
|
|
|
hasError || isSampled ? styles.logsRowWithError : `${levelStyles.logsRowLevelColor} ${styles.logsRowLevel}`
|
|
|
|
}
|
2022-09-19 16:51:46 +08:00
|
|
|
>
|
2024-12-19 02:03:47 +08:00
|
|
|
{hasError && (
|
2025-04-30 21:40:15 +08:00
|
|
|
<Tooltip
|
|
|
|
content={t('logs.log-row-message.tooltip-error', 'Error: {{errorMessage}}', { errorMessage })}
|
|
|
|
placement="right"
|
|
|
|
theme="error"
|
|
|
|
>
|
2024-12-19 02:03:47 +08:00
|
|
|
<Icon className={styles.logIconError} name="exclamation-triangle" size="xs" />
|
|
|
|
</Tooltip>
|
2022-09-19 16:51:46 +08:00
|
|
|
)}
|
2024-12-19 02:03:47 +08:00
|
|
|
{isSampled && (
|
2025-04-30 21:40:15 +08:00
|
|
|
<Tooltip content={sampleMessage} placement="right" theme="info">
|
2024-12-19 02:03:47 +08:00
|
|
|
<Icon className={styles.logIconInfo} name="info-circle" size="xs" />
|
|
|
|
</Tooltip>
|
2022-09-19 16:51:46 +08:00
|
|
|
)}
|
2024-12-19 02:03:47 +08:00
|
|
|
</td>
|
|
|
|
<td
|
|
|
|
title={enableLogDetails ? (showDetails ? 'Hide log details' : 'See log details') : ''}
|
|
|
|
className={enableLogDetails ? styles.logsRowToggleDetails : ''}
|
|
|
|
>
|
|
|
|
{enableLogDetails && (
|
2025-01-28 02:05:50 +08:00
|
|
|
<button
|
|
|
|
aria-label={t('logs.log-row-message.see-details', `See log details`)}
|
|
|
|
className={styles.detailsToggle}
|
|
|
|
aria-expanded={showDetails}
|
|
|
|
>
|
|
|
|
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
|
|
|
|
</button>
|
2022-09-19 16:51:46 +08:00
|
|
|
)}
|
2024-12-19 02:03:47 +08:00
|
|
|
</td>
|
|
|
|
{showTime && <td className={styles.logsRowLocalTime}>{timestamp}</td>}
|
|
|
|
{showLabels && processedRow.uniqueLabels && (
|
|
|
|
<td className={styles.logsRowLabels}>
|
|
|
|
<LogLabels labels={processedRow.uniqueLabels} addTooltip={false} />
|
|
|
|
</td>
|
|
|
|
)}
|
|
|
|
{displayedFields && displayedFields.length > 0 ? (
|
|
|
|
<LogRowMessageDisplayedFields
|
|
|
|
row={processedRow}
|
|
|
|
showContextToggle={showContextToggle}
|
|
|
|
detectedFields={displayedFields}
|
2022-09-19 16:51:46 +08:00
|
|
|
getFieldLinks={getFieldLinks}
|
2024-12-19 02:03:47 +08:00
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
onOpenContext={onOpenContext}
|
|
|
|
onPermalinkClick={props.onPermalinkClick}
|
|
|
|
styles={styles}
|
|
|
|
onPinLine={props.onPinLine}
|
|
|
|
onUnpinLine={props.onUnpinLine}
|
|
|
|
pinned={pinned}
|
|
|
|
mouseIsOver={mouseIsOver}
|
|
|
|
onBlur={onMouseLeave}
|
|
|
|
logRowMenuIconsBefore={logRowMenuIconsBefore}
|
|
|
|
logRowMenuIconsAfter={logRowMenuIconsAfter}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<LogRowMessage
|
2022-09-19 16:51:46 +08:00
|
|
|
row={processedRow}
|
2024-12-19 02:03:47 +08:00
|
|
|
showContextToggle={showContextToggle}
|
|
|
|
getRowContextQuery={getRowContextQuery}
|
2022-09-19 16:51:46 +08:00
|
|
|
wrapLogMessage={wrapLogMessage}
|
2024-12-19 02:03:47 +08:00
|
|
|
prettifyLogMessage={prettifyLogMessage}
|
|
|
|
onOpenContext={onOpenContext}
|
|
|
|
onPermalinkClick={props.onPermalinkClick}
|
2022-10-13 21:26:59 +08:00
|
|
|
app={app}
|
2023-03-03 17:19:48 +08:00
|
|
|
styles={styles}
|
2024-12-19 02:03:47 +08:00
|
|
|
onPinLine={props.onPinLine}
|
|
|
|
onUnpinLine={props.onUnpinLine}
|
|
|
|
pinLineButtonTooltipTitle={props.pinLineButtonTooltipTitle}
|
|
|
|
pinned={pinned}
|
|
|
|
mouseIsOver={mouseIsOver}
|
|
|
|
onBlur={onMouseLeave}
|
|
|
|
expanded={showDetails}
|
2025-05-14 21:06:19 +08:00
|
|
|
forceEscape={forceEscape}
|
2024-12-19 02:03:47 +08:00
|
|
|
logRowMenuIconsBefore={logRowMenuIconsBefore}
|
|
|
|
logRowMenuIconsAfter={logRowMenuIconsAfter}
|
2022-09-19 16:51:46 +08:00
|
|
|
/>
|
|
|
|
)}
|
2024-12-19 02:03:47 +08:00
|
|
|
</tr>
|
|
|
|
{showDetails && (
|
|
|
|
<LogDetails
|
|
|
|
onPinLine={props.onPinLine}
|
|
|
|
className={`${styles.logsRow} ${hasError ? styles.errorLogRow : ''} ${permalinked && !showDetails ? styles.highlightBackground : ''}`}
|
|
|
|
showDuplicates={showDuplicates}
|
|
|
|
getFieldLinks={getFieldLinks}
|
|
|
|
onClickFilterLabel={onClickFilterLabel}
|
|
|
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
|
|
|
onClickShowField={onClickShowField}
|
|
|
|
onClickHideField={onClickHideField}
|
|
|
|
getRows={getRows}
|
|
|
|
row={processedRow}
|
|
|
|
wrapLogMessage={wrapLogMessage}
|
|
|
|
hasError={hasError}
|
|
|
|
displayedFields={displayedFields}
|
|
|
|
app={app}
|
|
|
|
styles={styles}
|
|
|
|
isFilterLabelActive={props.isFilterLabelActive}
|
|
|
|
pinLineButtonTooltipTitle={props.pinLineButtonTooltipTitle}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|