2023-09-12 22:57:14 +08:00
|
|
|
import { cx } from '@emotion/css';
|
2022-09-19 16:51:46 +08:00
|
|
|
import memoizeOne from 'memoize-one';
|
2023-11-16 17:48:10 +08:00
|
|
|
import React, { PureComponent, MouseEvent, createRef } from 'react';
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2022-11-10 22:33:17 +08:00
|
|
|
import {
|
|
|
|
|
TimeZone,
|
|
|
|
|
LogsDedupStrategy,
|
|
|
|
|
LogRowModel,
|
|
|
|
|
Field,
|
|
|
|
|
LinkModel,
|
|
|
|
|
LogsSortOrder,
|
|
|
|
|
CoreApp,
|
|
|
|
|
DataFrame,
|
|
|
|
|
} from '@grafana/data';
|
2023-11-16 17:48:10 +08:00
|
|
|
import { config } from '@grafana/runtime';
|
2022-09-19 16:51:46 +08:00
|
|
|
import { withTheme2, Themeable2 } from '@grafana/ui';
|
|
|
|
|
|
2023-11-28 21:21:37 +08:00
|
|
|
import { PopoverMenu } from '../../explore/Logs/PopoverMenu';
|
2023-07-13 14:01:55 +08:00
|
|
|
import { UniqueKeyMaker } from '../UniqueKeyMaker';
|
2023-11-16 17:48:10 +08:00
|
|
|
import { sortLogRows, targetIsElement } from '../utils';
|
2022-09-30 18:16:47 +08:00
|
|
|
|
2022-09-19 16:51:46 +08:00
|
|
|
//Components
|
|
|
|
|
import { LogRow } from './LogRow';
|
|
|
|
|
import { getLogRowStyles } from './getLogRowStyles';
|
|
|
|
|
|
|
|
|
|
export const PREVIEW_LIMIT = 100;
|
|
|
|
|
|
|
|
|
|
export interface Props extends Themeable2 {
|
|
|
|
|
logRows?: LogRowModel[];
|
|
|
|
|
deduplicatedRows?: LogRowModel[];
|
|
|
|
|
dedupStrategy: LogsDedupStrategy;
|
|
|
|
|
showLabels: boolean;
|
|
|
|
|
showTime: boolean;
|
|
|
|
|
wrapLogMessage: boolean;
|
|
|
|
|
prettifyLogMessage: boolean;
|
|
|
|
|
timeZone: TimeZone;
|
|
|
|
|
enableLogDetails: boolean;
|
|
|
|
|
logsSortOrder?: LogsSortOrder | null;
|
|
|
|
|
previewLimit?: number;
|
|
|
|
|
forceEscape?: boolean;
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields?: string[];
|
2022-09-29 16:00:01 +08:00
|
|
|
app?: CoreApp;
|
2023-10-31 19:37:44 +08:00
|
|
|
showContextToggle?: (row: LogRowModel) => boolean;
|
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-11-10 22:33:17 +08:00
|
|
|
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
2023-01-12 02:20:11 +08:00
|
|
|
onClickShowField?: (key: string) => void;
|
|
|
|
|
onClickHideField?: (key: string) => void;
|
2023-06-28 21:22:54 +08:00
|
|
|
onPinLine?: (row: LogRowModel) => void;
|
|
|
|
|
onUnpinLine?: (row: LogRowModel) => 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-06-16 20:07:51 +08:00
|
|
|
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
|
|
|
|
permalinkedRowId?: string;
|
|
|
|
|
scrollIntoView?: (element: HTMLElement) => void;
|
2023-09-04 22:30:17 +08:00
|
|
|
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
2023-06-28 21:22:54 +08:00
|
|
|
pinnedRowId?: string;
|
2023-08-18 18:54:08 +08:00
|
|
|
containerRendered?: boolean;
|
2023-09-12 22:57:14 +08:00
|
|
|
/**
|
|
|
|
|
* If false or undefined, the `contain:strict` css property will be added to the wrapping `<table>` for performance reasons.
|
|
|
|
|
* Any overflowing content will be clipped at the table boundary.
|
|
|
|
|
*/
|
|
|
|
|
overflowingContent?: boolean;
|
2023-11-16 17:48:10 +08:00
|
|
|
onClickFilterValue?: (value: string, refId?: string) => void;
|
|
|
|
|
onClickFilterOutValue?: (value: string, refId?: string) => void;
|
2022-09-19 16:51:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
|
renderAll: boolean;
|
2023-11-16 17:48:10 +08:00
|
|
|
selection: string;
|
|
|
|
|
selectedRow: LogRowModel | null;
|
|
|
|
|
popoverMenuCoordinates: { x: number; y: number };
|
2022-09-19 16:51:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class UnThemedLogRows extends PureComponent<Props, State> {
|
|
|
|
|
renderAllTimer: number | null = null;
|
2023-11-16 17:48:10 +08:00
|
|
|
logRowsRef = createRef<HTMLDivElement>();
|
2022-09-19 16:51:46 +08:00
|
|
|
|
|
|
|
|
static defaultProps = {
|
|
|
|
|
previewLimit: PREVIEW_LIMIT,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
state: State = {
|
|
|
|
|
renderAll: false,
|
2023-11-16 17:48:10 +08:00
|
|
|
selection: '',
|
|
|
|
|
selectedRow: null,
|
|
|
|
|
popoverMenuCoordinates: { x: 0, y: 0 },
|
2022-09-29 16:00:01 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Toggle the `contextIsOpen` state when a context of one LogRow is opened in order to not show the menu of the other log rows.
|
|
|
|
|
*/
|
2023-04-14 23:05:43 +08:00
|
|
|
openContext = (row: LogRowModel, onClose: () => void): void => {
|
|
|
|
|
if (this.props.onOpenContext) {
|
|
|
|
|
this.props.onOpenContext(row, onClose);
|
|
|
|
|
}
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
|
|
|
|
|
2023-11-16 17:48:10 +08:00
|
|
|
popoverMenuSupported() {
|
2023-11-28 21:21:37 +08:00
|
|
|
if (!config.featureToggles.logRowsPopoverMenu || this.props.app !== CoreApp.Explore) {
|
2023-11-16 17:48:10 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return Boolean(this.props.onClickFilterOutValue || this.props.onClickFilterValue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleSelection = (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel): boolean => {
|
|
|
|
|
if (this.popoverMenuSupported() === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const selection = document.getSelection()?.toString();
|
|
|
|
|
if (!selection) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!this.logRowsRef.current) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const parentBounds = this.logRowsRef.current?.getBoundingClientRect();
|
|
|
|
|
this.setState({
|
|
|
|
|
selection,
|
|
|
|
|
popoverMenuCoordinates: { x: e.clientX - parentBounds.left, y: e.clientY - parentBounds.top },
|
|
|
|
|
selectedRow: row,
|
|
|
|
|
});
|
2023-11-23 18:04:23 +08:00
|
|
|
document.addEventListener('click', this.handleDeselection);
|
2023-11-27 20:36:55 +08:00
|
|
|
document.addEventListener('contextmenu', this.handleDeselection);
|
2023-11-16 17:48:10 +08:00
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
handleDeselection = (e: Event) => {
|
|
|
|
|
if (targetIsElement(e.target) && !this.logRowsRef.current?.contains(e.target)) {
|
|
|
|
|
// The mouseup event comes from outside the log rows, close the menu.
|
|
|
|
|
this.closePopoverMenu();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (document.getSelection()?.toString()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.closePopoverMenu();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
closePopoverMenu = () => {
|
2023-11-23 18:04:23 +08:00
|
|
|
document.removeEventListener('click', this.handleDeselection);
|
2023-11-27 20:36:55 +08:00
|
|
|
document.removeEventListener('contextmenu', this.handleDeselection);
|
2023-11-16 17:48:10 +08:00
|
|
|
this.setState({
|
|
|
|
|
selection: '',
|
|
|
|
|
popoverMenuCoordinates: { x: 0, y: 0 },
|
|
|
|
|
selectedRow: null,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2022-09-19 16:51:46 +08:00
|
|
|
componentDidMount() {
|
|
|
|
|
// Staged rendering
|
|
|
|
|
const { logRows, previewLimit } = this.props;
|
|
|
|
|
const rowCount = logRows ? logRows.length : 0;
|
|
|
|
|
// Render all right away if not too far over the limit
|
|
|
|
|
const renderAll = rowCount <= previewLimit! * 2;
|
|
|
|
|
if (renderAll) {
|
|
|
|
|
this.setState({ renderAll });
|
|
|
|
|
} else {
|
|
|
|
|
this.renderAllTimer = window.setTimeout(() => this.setState({ renderAll: true }), 2000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2023-11-23 18:04:23 +08:00
|
|
|
document.removeEventListener('click', this.handleDeselection);
|
2023-11-27 20:36:55 +08:00
|
|
|
document.removeEventListener('contextmenu', this.handleDeselection);
|
2022-09-19 16:51:46 +08:00
|
|
|
if (this.renderAllTimer) {
|
|
|
|
|
clearTimeout(this.renderAllTimer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
makeGetRows = memoizeOne((orderedRows: LogRowModel[]) => {
|
|
|
|
|
return () => orderedRows;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sortLogs = memoizeOne((logRows: LogRowModel[], logsSortOrder: LogsSortOrder): LogRowModel[] =>
|
|
|
|
|
sortLogRows(logRows, logsSortOrder)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render() {
|
2023-06-28 21:45:22 +08:00
|
|
|
const { deduplicatedRows, logRows, dedupStrategy, theme, logsSortOrder, previewLimit, ...rest } = this.props;
|
2023-04-14 23:05:43 +08:00
|
|
|
const { renderAll } = this.state;
|
2023-02-01 22:28:10 +08:00
|
|
|
const styles = getLogRowStyles(theme);
|
2022-09-19 16:51:46 +08:00
|
|
|
const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows;
|
|
|
|
|
const hasData = logRows && logRows.length > 0;
|
|
|
|
|
const dedupCount = dedupedRows
|
|
|
|
|
? dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
|
|
|
|
|
: 0;
|
|
|
|
|
const showDuplicates = dedupStrategy !== LogsDedupStrategy.none && dedupCount > 0;
|
|
|
|
|
// Staged rendering
|
|
|
|
|
const processedRows = dedupedRows ? dedupedRows : [];
|
|
|
|
|
const orderedRows = logsSortOrder ? this.sortLogs(processedRows, logsSortOrder) : processedRows;
|
|
|
|
|
const firstRows = orderedRows.slice(0, previewLimit!);
|
|
|
|
|
const lastRows = orderedRows.slice(previewLimit!, orderedRows.length);
|
|
|
|
|
|
|
|
|
|
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
|
|
|
|
|
const getRows = this.makeGetRows(orderedRows);
|
|
|
|
|
|
2023-07-13 14:01:55 +08:00
|
|
|
const keyMaker = new UniqueKeyMaker();
|
|
|
|
|
|
2022-09-19 16:51:46 +08:00
|
|
|
return (
|
2023-11-16 17:48:10 +08:00
|
|
|
<div className={styles.logRows} ref={this.logRowsRef}>
|
|
|
|
|
{this.state.selection && this.state.selectedRow && (
|
|
|
|
|
<PopoverMenu
|
|
|
|
|
close={this.closePopoverMenu}
|
|
|
|
|
row={this.state.selectedRow}
|
|
|
|
|
selection={this.state.selection}
|
|
|
|
|
{...this.state.popoverMenuCoordinates}
|
|
|
|
|
onClickFilterValue={rest.onClickFilterValue}
|
|
|
|
|
onClickFilterOutValue={rest.onClickFilterOutValue}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<table className={cx(styles.logsRowsTable, this.props.overflowingContent ? '' : styles.logsRowsTableContain)}>
|
|
|
|
|
<tbody>
|
|
|
|
|
{hasData &&
|
|
|
|
|
firstRows.map((row) => (
|
|
|
|
|
<LogRow
|
|
|
|
|
key={keyMaker.getKey(row.uid)}
|
|
|
|
|
getRows={getRows}
|
|
|
|
|
row={row}
|
|
|
|
|
showDuplicates={showDuplicates}
|
|
|
|
|
logsSortOrder={logsSortOrder}
|
|
|
|
|
onOpenContext={this.openContext}
|
|
|
|
|
styles={styles}
|
|
|
|
|
onPermalinkClick={this.props.onPermalinkClick}
|
|
|
|
|
scrollIntoView={this.props.scrollIntoView}
|
|
|
|
|
permalinkedRowId={this.props.permalinkedRowId}
|
|
|
|
|
onPinLine={this.props.onPinLine}
|
|
|
|
|
onUnpinLine={this.props.onUnpinLine}
|
|
|
|
|
pinned={this.props.pinnedRowId === row.uid}
|
|
|
|
|
isFilterLabelActive={this.props.isFilterLabelActive}
|
|
|
|
|
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
|
|
|
|
{...rest}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
{hasData &&
|
|
|
|
|
renderAll &&
|
|
|
|
|
lastRows.map((row) => (
|
|
|
|
|
<LogRow
|
|
|
|
|
key={keyMaker.getKey(row.uid)}
|
|
|
|
|
getRows={getRows}
|
|
|
|
|
row={row}
|
|
|
|
|
showDuplicates={showDuplicates}
|
|
|
|
|
logsSortOrder={logsSortOrder}
|
|
|
|
|
onOpenContext={this.openContext}
|
|
|
|
|
styles={styles}
|
|
|
|
|
onPermalinkClick={this.props.onPermalinkClick}
|
|
|
|
|
scrollIntoView={this.props.scrollIntoView}
|
|
|
|
|
permalinkedRowId={this.props.permalinkedRowId}
|
|
|
|
|
onPinLine={this.props.onPinLine}
|
|
|
|
|
onUnpinLine={this.props.onUnpinLine}
|
|
|
|
|
pinned={this.props.pinnedRowId === row.uid}
|
|
|
|
|
isFilterLabelActive={this.props.isFilterLabelActive}
|
|
|
|
|
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
|
|
|
|
{...rest}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
{hasData && !renderAll && (
|
|
|
|
|
<tr>
|
|
|
|
|
<td colSpan={5}>Rendering {orderedRows.length - previewLimit!} rows...</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
2022-09-19 16:51:46 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const LogRows = withTheme2(UnThemedLogRows);
|
|
|
|
|
LogRows.displayName = 'LogsRows';
|