mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
* add permalink tracking * move location override to beforeAll * add `logLevel` to tracking
267 lines
8.6 KiB
TypeScript
267 lines
8.6 KiB
TypeScript
import { cx } from '@emotion/css';
|
|
import { debounce } from 'lodash';
|
|
import React, { PureComponent } from 'react';
|
|
|
|
import { Field, LinkModel, LogRowModel, LogsSortOrder, dateTimeFormat, CoreApp, DataFrame } from '@grafana/data';
|
|
import { reportInteraction } from '@grafana/runtime';
|
|
import { TimeZone } from '@grafana/schema';
|
|
import { withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui';
|
|
|
|
import { checkLogsError, escapeUnescapedString } from '../utils';
|
|
|
|
import { LogDetails } from './LogDetails';
|
|
import { LogLabels } from './LogLabels';
|
|
import { LogRowMessage } from './LogRowMessage';
|
|
import { LogRowMessageDisplayedFields } from './LogRowMessageDisplayedFields';
|
|
import { getLogLevelStyles, LogRowStyles } from './getLogRowStyles';
|
|
|
|
interface Props extends Themeable2 {
|
|
row: LogRowModel;
|
|
showDuplicates: boolean;
|
|
showLabels: boolean;
|
|
showTime: boolean;
|
|
wrapLogMessage: boolean;
|
|
prettifyLogMessage: boolean;
|
|
timeZone: TimeZone;
|
|
enableLogDetails: boolean;
|
|
logsSortOrder?: LogsSortOrder | null;
|
|
forceEscape?: boolean;
|
|
app?: CoreApp;
|
|
displayedFields?: string[];
|
|
getRows: () => LogRowModel[];
|
|
onClickFilterLabel?: (key: string, value: string) => void;
|
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
|
onContextClick?: () => void;
|
|
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
|
showContextToggle?: (row?: LogRowModel) => boolean;
|
|
onClickShowField?: (key: string) => void;
|
|
onClickHideField?: (key: string) => void;
|
|
onLogRowHover?: (row?: LogRowModel) => void;
|
|
onOpenContext: (row: LogRowModel, onClose: () => void) => void;
|
|
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
|
styles: LogRowStyles;
|
|
permalinkedRowId?: string;
|
|
scrollIntoView?: (element: HTMLElement) => void;
|
|
}
|
|
|
|
interface State {
|
|
highlightBackround: boolean;
|
|
showDetails: boolean;
|
|
}
|
|
|
|
/**
|
|
* Renders a log line.
|
|
*
|
|
* When user hovers over it for a certain time, it lazily parses the log line.
|
|
* Once a parser is found, it will determine fields, that will be highlighted.
|
|
* When the user requests stats for a field, they will be calculated and rendered below the row.
|
|
*/
|
|
class UnThemedLogRow extends PureComponent<Props, State> {
|
|
state: State = {
|
|
highlightBackround: false,
|
|
showDetails: false,
|
|
};
|
|
logLineRef: React.RefObject<HTMLTableRowElement>;
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.logLineRef = React.createRef();
|
|
}
|
|
|
|
// we are debouncing the state change by 3 seconds to highlight the logline after the context closed.
|
|
debouncedContextClose = debounce(() => {
|
|
this.setState({ highlightBackround: false });
|
|
}, 3000);
|
|
|
|
onOpenContext = (row: LogRowModel) => {
|
|
this.setState({ highlightBackround: true });
|
|
this.props.onOpenContext(row, this.debouncedContextClose);
|
|
};
|
|
|
|
toggleDetails = () => {
|
|
if (!this.props.enableLogDetails) {
|
|
return;
|
|
}
|
|
|
|
reportInteraction('grafana_explore_logs_log_details_clicked', {
|
|
datasourceType: this.props.row.datasourceType,
|
|
type: this.state.showDetails ? 'close' : 'open',
|
|
logRowUid: this.props.row.uid,
|
|
app: this.props.app,
|
|
});
|
|
|
|
this.setState((state) => {
|
|
return {
|
|
showDetails: !state.showDetails,
|
|
};
|
|
});
|
|
};
|
|
|
|
renderTimeStamp(epochMs: number) {
|
|
return dateTimeFormat(epochMs, {
|
|
timeZone: this.props.timeZone,
|
|
defaultWithMS: true,
|
|
});
|
|
}
|
|
|
|
onMouseEnter = () => {
|
|
if (this.props.onLogRowHover) {
|
|
this.props.onLogRowHover(this.props.row);
|
|
}
|
|
};
|
|
|
|
onMouseLeave = () => {
|
|
if (this.props.onLogRowHover) {
|
|
this.props.onLogRowHover(undefined);
|
|
}
|
|
};
|
|
|
|
componentDidMount() {
|
|
this.scrollToLogRow(this.state, true);
|
|
}
|
|
|
|
componentDidUpdate(_: Props, prevState: State) {
|
|
this.scrollToLogRow(prevState);
|
|
}
|
|
|
|
scrollToLogRow = (prevState: State, mounted = false) => {
|
|
const { row, permalinkedRowId, scrollIntoView } = this.props;
|
|
|
|
if (permalinkedRowId !== row.uid) {
|
|
// only set the new state if the row is not permalinked anymore or if the component was mounted.
|
|
if (prevState.highlightBackround || mounted) {
|
|
this.setState({ highlightBackround: false });
|
|
}
|
|
return;
|
|
}
|
|
|
|
// at this point this row is the permalinked row, so we need to scroll to it and highlight it if possible.
|
|
if (this.logLineRef.current && scrollIntoView) {
|
|
scrollIntoView(this.logLineRef.current);
|
|
}
|
|
if (!this.state.highlightBackround) {
|
|
reportInteraction('grafana_explore_logs_permalink_opened', {
|
|
datasourceType: row.datasourceType ?? 'unknown',
|
|
logRowUid: row.uid,
|
|
});
|
|
this.setState({ highlightBackround: true });
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
getRows,
|
|
onClickFilterLabel,
|
|
onClickFilterOutLabel,
|
|
onClickShowField,
|
|
onClickHideField,
|
|
enableLogDetails,
|
|
row,
|
|
showDuplicates,
|
|
showContextToggle,
|
|
showLabels,
|
|
showTime,
|
|
displayedFields,
|
|
wrapLogMessage,
|
|
prettifyLogMessage,
|
|
theme,
|
|
getFieldLinks,
|
|
forceEscape,
|
|
app,
|
|
styles,
|
|
} = this.props;
|
|
const { showDetails, highlightBackround } = this.state;
|
|
const levelStyles = getLogLevelStyles(theme, row.logLevel);
|
|
const { errorMessage, hasError } = checkLogsError(row);
|
|
const logRowBackground = cx(styles.logsRow, {
|
|
[styles.errorLogRow]: hasError,
|
|
[styles.highlightBackground]: highlightBackround,
|
|
});
|
|
const logRowDetailsBackground = cx(styles.logsRow, {
|
|
[styles.errorLogRow]: hasError,
|
|
[styles.highlightBackground]: highlightBackround && !this.state.showDetails,
|
|
});
|
|
|
|
const processedRow =
|
|
row.hasUnescapedContent && forceEscape
|
|
? { ...row, entry: escapeUnescapedString(row.entry), raw: escapeUnescapedString(row.raw) }
|
|
: row;
|
|
|
|
return (
|
|
<>
|
|
<tr
|
|
ref={this.logLineRef}
|
|
className={logRowBackground}
|
|
onClick={this.toggleDetails}
|
|
onMouseEnter={this.onMouseEnter}
|
|
onMouseLeave={this.onMouseLeave}
|
|
>
|
|
{showDuplicates && (
|
|
<td className={styles.logsRowDuplicates}>
|
|
{processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null}
|
|
</td>
|
|
)}
|
|
<td className={hasError ? '' : `${levelStyles.logsRowLevelColor} ${styles.logsRowLevel}`}>
|
|
{hasError && (
|
|
<Tooltip content={`Error: ${errorMessage}`} placement="right" theme="error">
|
|
<Icon className={styles.logIconError} name="exclamation-triangle" size="xs" />
|
|
</Tooltip>
|
|
)}
|
|
</td>
|
|
{enableLogDetails && (
|
|
<td title={showDetails ? 'Hide log details' : 'See log details'} className={styles.logsRowToggleDetails}>
|
|
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
|
|
</td>
|
|
)}
|
|
{showTime && <td className={styles.logsRowLocalTime}>{this.renderTimeStamp(row.timeEpochMs)}</td>}
|
|
{showLabels && processedRow.uniqueLabels && (
|
|
<td className={styles.logsRowLabels}>
|
|
<LogLabels labels={processedRow.uniqueLabels} />
|
|
</td>
|
|
)}
|
|
{displayedFields && displayedFields.length > 0 ? (
|
|
<LogRowMessageDisplayedFields
|
|
row={processedRow}
|
|
showDetectedFields={displayedFields!}
|
|
getFieldLinks={getFieldLinks}
|
|
wrapLogMessage={wrapLogMessage}
|
|
/>
|
|
) : (
|
|
<LogRowMessage
|
|
row={processedRow}
|
|
showContextToggle={showContextToggle}
|
|
wrapLogMessage={wrapLogMessage}
|
|
prettifyLogMessage={prettifyLogMessage}
|
|
onOpenContext={this.onOpenContext}
|
|
onPermalinkClick={this.props.onPermalinkClick}
|
|
app={app}
|
|
styles={styles}
|
|
/>
|
|
)}
|
|
</tr>
|
|
{this.state.showDetails && (
|
|
<LogDetails
|
|
className={logRowDetailsBackground}
|
|
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}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
export const LogRow = withTheme2(UnThemedLogRow);
|
|
LogRow.displayName = 'LogRow';
|