mirror of
https://github.com/grafana/grafana.git
synced 2025-01-08 23:23:45 -06:00
Logs: Display log row menu cell on displayed fields (#71300)
* LogRowMenuCell: create component * LogRowMessage: use new LogRowMenuCell component * LogRowMessage: turn into functional component * LogRowMenuCell: memoize component * LogRowMessage: remove cx * LogMessage: create component from function * LogRowMessageDisplayedFields: turn into component * LogRowMessageDisplayedFields: add LogRowMenuCell * LogRowMessageDisplayedFields: rename prop and pass missing context prop * LogRowMessageDisplayedFields: add unit test
This commit is contained in:
parent
724e46a8a7
commit
b8fbeb084a
@ -225,9 +225,16 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
{displayedFields && displayedFields.length > 0 ? (
|
||||
<LogRowMessageDisplayedFields
|
||||
row={processedRow}
|
||||
showDetectedFields={displayedFields!}
|
||||
showContextToggle={showContextToggle}
|
||||
detectedFields={displayedFields}
|
||||
getFieldLinks={getFieldLinks}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
onOpenContext={this.onOpenContext}
|
||||
onPermalinkClick={this.props.onPermalinkClick}
|
||||
styles={styles}
|
||||
onPinLine={this.props.onPinLine}
|
||||
onUnpinLine={this.props.onUnpinLine}
|
||||
pinned={this.props.pinned}
|
||||
/>
|
||||
) : (
|
||||
<LogRowMessage
|
||||
|
122
public/app/features/logs/components/LogRowMenuCell.tsx
Normal file
122
public/app/features/logs/components/LogRowMenuCell.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { SyntheticEvent, useCallback } from 'react';
|
||||
|
||||
import { LogRowModel } from '@grafana/data';
|
||||
import { ClipboardButton, IconButton } from '@grafana/ui';
|
||||
|
||||
import { LogRowStyles } from './getLogRowStyles';
|
||||
|
||||
interface Props {
|
||||
logText: string;
|
||||
row: LogRowModel;
|
||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||
onOpenContext: (row: LogRowModel) => void;
|
||||
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinned?: boolean;
|
||||
styles: LogRowStyles;
|
||||
}
|
||||
|
||||
export const LogRowMenuCell = React.memo(
|
||||
({
|
||||
logText,
|
||||
onOpenContext,
|
||||
onPermalinkClick,
|
||||
onPinLine,
|
||||
onUnpinLine,
|
||||
pinned,
|
||||
row,
|
||||
showContextToggle,
|
||||
styles,
|
||||
}: Props) => {
|
||||
const shouldShowContextToggle = showContextToggle ? showContextToggle(row) : false;
|
||||
const onLogRowClick = useCallback((e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
const onShowContextClick = useCallback(
|
||||
(e: SyntheticEvent<HTMLElement, Event>) => {
|
||||
e.stopPropagation();
|
||||
onOpenContext(row);
|
||||
},
|
||||
[onOpenContext, row]
|
||||
);
|
||||
const getLogText = useCallback(() => logText, [logText]);
|
||||
return (
|
||||
<>
|
||||
{pinned && (
|
||||
// TODO: fix keyboard a11y
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<span className={`log-row-menu log-row-menu-visible ${styles.rowMenu}`} onClick={onLogRowClick}>
|
||||
<IconButton
|
||||
className={styles.unPinButton}
|
||||
size="md"
|
||||
name="gf-pin"
|
||||
onClick={() => onUnpinLine && onUnpinLine(row)}
|
||||
tooltip="Unpin line"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Unpin line"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{/* TODO: fix keyboard a11y */}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<span className={`log-row-menu ${styles.rowMenu} ${styles.hidden}`} onClick={onLogRowClick}>
|
||||
{shouldShowContextToggle && (
|
||||
<IconButton
|
||||
size="md"
|
||||
name="gf-show-context"
|
||||
onClick={onShowContextClick}
|
||||
tooltip="Show context"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show context"
|
||||
/>
|
||||
)}
|
||||
<ClipboardButton
|
||||
className={styles.copyLogButton}
|
||||
icon="copy"
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
size="md"
|
||||
getText={getLogText}
|
||||
tooltip="Copy to clipboard"
|
||||
tooltipPlacement="top"
|
||||
/>
|
||||
{pinned && onUnpinLine && (
|
||||
<IconButton
|
||||
className={styles.unPinButton}
|
||||
size="md"
|
||||
name="gf-pin"
|
||||
onClick={() => onUnpinLine && onUnpinLine(row)}
|
||||
tooltip="Unpin line"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Unpin line"
|
||||
/>
|
||||
)}
|
||||
{!pinned && onPinLine && (
|
||||
<IconButton
|
||||
className={styles.unPinButton}
|
||||
size="md"
|
||||
name="gf-pin"
|
||||
onClick={() => onPinLine && onPinLine(row)}
|
||||
tooltip="Pin line"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Pin line"
|
||||
/>
|
||||
)}
|
||||
{onPermalinkClick && row.uid && (
|
||||
<IconButton
|
||||
tooltip="Copy shortlink"
|
||||
aria-label="Copy shortlink"
|
||||
tooltipPlacement="top"
|
||||
size="md"
|
||||
name="share-alt"
|
||||
onClick={() => onPermalinkClick(row)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LogRowMenuCell.displayName = 'LogRowMenuCell';
|
@ -1,12 +1,10 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
|
||||
import { CoreApp, findHighlightChunksInText, LogRowModel } from '@grafana/data';
|
||||
import { ClipboardButton, IconButton } from '@grafana/ui';
|
||||
|
||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||
import { LogRowMenuCell } from './LogRowMenuCell';
|
||||
import { LogRowStyles } from './getLogRowStyles';
|
||||
|
||||
export const MAX_CHARACTERS = 100000;
|
||||
@ -25,17 +23,19 @@ interface Props {
|
||||
styles: LogRowStyles;
|
||||
}
|
||||
|
||||
function renderLogMessage(
|
||||
hasAnsi: boolean,
|
||||
entry: string,
|
||||
highlights: string[] | undefined,
|
||||
highlightClassName: string
|
||||
) {
|
||||
interface LogMessageProps {
|
||||
hasAnsi: boolean;
|
||||
entry: string;
|
||||
highlights: string[] | undefined;
|
||||
styles: LogRowStyles;
|
||||
}
|
||||
|
||||
const LogMessage = ({ hasAnsi, entry, highlights, styles }: LogMessageProps) => {
|
||||
const needsHighlighter =
|
||||
highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0 && entry.length < MAX_CHARACTERS;
|
||||
const searchWords = highlights ?? [];
|
||||
if (hasAnsi) {
|
||||
const highlight = needsHighlighter ? { searchWords, highlightClassName } : undefined;
|
||||
const highlight = needsHighlighter ? { searchWords, highlightClassName: styles.logsRowMatchHighLight } : undefined;
|
||||
return <LogMessageAnsi value={entry} highlight={highlight} />;
|
||||
} else if (needsHighlighter) {
|
||||
return (
|
||||
@ -43,15 +43,14 @@ function renderLogMessage(
|
||||
textToHighlight={entry}
|
||||
searchWords={searchWords}
|
||||
findChunks={findHighlightChunksInText}
|
||||
highlightClassName={highlightClassName}
|
||||
highlightClassName={styles.logsRowMatchHighLight}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return <>{entry}</>;
|
||||
};
|
||||
|
||||
const restructureLog = memoizeOne((line: string, prettifyLogMessage: boolean): string => {
|
||||
const restructureLog = (line: string, prettifyLogMessage: boolean): string => {
|
||||
if (prettifyLogMessage) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(line), undefined, 2);
|
||||
@ -60,133 +59,51 @@ const restructureLog = memoizeOne((line: string, prettifyLogMessage: boolean): s
|
||||
}
|
||||
}
|
||||
return line;
|
||||
};
|
||||
|
||||
export const LogRowMessage = React.memo((props: Props) => {
|
||||
const {
|
||||
row,
|
||||
wrapLogMessage,
|
||||
prettifyLogMessage,
|
||||
showContextToggle,
|
||||
styles,
|
||||
onOpenContext,
|
||||
onPermalinkClick,
|
||||
onUnpinLine,
|
||||
onPinLine,
|
||||
pinned,
|
||||
} = props;
|
||||
const { hasAnsi, raw } = row;
|
||||
const restructuredEntry = useMemo(() => restructureLog(raw, prettifyLogMessage), [raw, prettifyLogMessage]);
|
||||
return (
|
||||
<>
|
||||
{
|
||||
// When context is open, the position has to be NOT relative. // Setting the postion as inline-style to
|
||||
// overwrite the more sepecific style definition from `styles.logsRowMessage`.
|
||||
}
|
||||
<td className={styles.logsRowMessage}>
|
||||
<div className={wrapLogMessage ? styles.positionRelative : styles.horizontalScroll}>
|
||||
<button className={`${styles.logLine} ${styles.positionRelative}`}>
|
||||
<LogMessage hasAnsi={hasAnsi} entry={restructuredEntry} highlights={row.searchWords} styles={styles} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className={`log-row-menu-cell ${styles.logRowMenuCell}`}>
|
||||
<LogRowMenuCell
|
||||
logText={restructuredEntry}
|
||||
row={row}
|
||||
showContextToggle={showContextToggle}
|
||||
onOpenContext={onOpenContext}
|
||||
onPermalinkClick={onPermalinkClick}
|
||||
onPinLine={onPinLine}
|
||||
onUnpinLine={onUnpinLine}
|
||||
pinned={pinned}
|
||||
styles={styles}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export class LogRowMessage extends PureComponent<Props> {
|
||||
onShowContextClick = (e: React.SyntheticEvent<HTMLElement, Event>) => {
|
||||
const { onOpenContext } = this.props;
|
||||
e.stopPropagation();
|
||||
onOpenContext(this.props.row);
|
||||
};
|
||||
|
||||
onLogRowClick = (e: React.SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
getLogText = () => {
|
||||
const { row, prettifyLogMessage } = this.props;
|
||||
const { raw } = row;
|
||||
return restructureLog(raw, prettifyLogMessage);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
row,
|
||||
wrapLogMessage,
|
||||
prettifyLogMessage,
|
||||
showContextToggle,
|
||||
styles,
|
||||
onPermalinkClick,
|
||||
onUnpinLine,
|
||||
onPinLine,
|
||||
pinned,
|
||||
} = this.props;
|
||||
const { hasAnsi, raw } = row;
|
||||
const restructuredEntry = restructureLog(raw, prettifyLogMessage);
|
||||
const shouldShowContextToggle = showContextToggle ? showContextToggle(row) : false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
// When context is open, the position has to be NOT relative. // Setting the postion as inline-style to
|
||||
// overwrite the more sepecific style definition from `styles.logsRowMessage`.
|
||||
}
|
||||
<td className={styles.logsRowMessage}>
|
||||
<div
|
||||
className={cx(
|
||||
{ [styles.positionRelative]: wrapLogMessage },
|
||||
{ [styles.horizontalScroll]: !wrapLogMessage }
|
||||
)}
|
||||
>
|
||||
<button className={cx(styles.logLine, styles.positionRelative)}>
|
||||
{renderLogMessage(hasAnsi, restructuredEntry, row.searchWords, styles.logsRowMatchHighLight)}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className={cx('log-row-menu-cell', styles.logRowMenuCell)}>
|
||||
{pinned && (
|
||||
// TODO: fix keyboard a11y
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<span className={cx('log-row-menu', 'log-row-menu-visible', styles.rowMenu)} onClick={this.onLogRowClick}>
|
||||
<IconButton
|
||||
className={styles.unPinButton}
|
||||
size="md"
|
||||
name="gf-pin"
|
||||
onClick={() => onUnpinLine && onUnpinLine(row)}
|
||||
tooltip="Unpin line"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Unpin line"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{/* TODO: fix keyboard a11y */}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<span className={cx('log-row-menu', styles.rowMenu, styles.hidden)} onClick={this.onLogRowClick}>
|
||||
{shouldShowContextToggle && (
|
||||
<IconButton
|
||||
size="md"
|
||||
name="gf-show-context"
|
||||
onClick={this.onShowContextClick}
|
||||
tooltip="Show context"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show context"
|
||||
/>
|
||||
)}
|
||||
<ClipboardButton
|
||||
className={styles.copyLogButton}
|
||||
icon="copy"
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
size="md"
|
||||
getText={this.getLogText}
|
||||
tooltip="Copy to clipboard"
|
||||
tooltipPlacement="top"
|
||||
/>
|
||||
{pinned && onUnpinLine && (
|
||||
<IconButton
|
||||
className={styles.unPinButton}
|
||||
size="md"
|
||||
name="gf-pin"
|
||||
onClick={() => onUnpinLine && onUnpinLine(row)}
|
||||
tooltip="Unpin line"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Unpin line"
|
||||
/>
|
||||
)}
|
||||
{!pinned && onPinLine && (
|
||||
<IconButton
|
||||
className={styles.unPinButton}
|
||||
size="md"
|
||||
name="gf-pin"
|
||||
onClick={() => onPinLine && onPinLine(row)}
|
||||
tooltip="Pin line"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Pin line"
|
||||
/>
|
||||
)}
|
||||
{onPermalinkClick && row.uid && (
|
||||
<IconButton
|
||||
tooltip="Copy shortlink"
|
||||
aria-label="Copy shortlink"
|
||||
tooltipPlacement="top"
|
||||
size="md"
|
||||
name="share-alt"
|
||||
onClick={() => onPermalinkClick(row)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
LogRowMessage.displayName = 'LogRowMessage';
|
||||
|
@ -0,0 +1,46 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { createTheme, LogLevel } from '@grafana/data';
|
||||
|
||||
import { LogRowMessageDisplayedFields, Props } from './LogRowMessageDisplayedFields';
|
||||
import { createLogRow } from './__mocks__/logRow';
|
||||
import { getLogRowStyles } from './getLogRowStyles';
|
||||
|
||||
const setup = (propOverrides: Partial<Props> = {}, detectedFields = ['place', 'planet']) => {
|
||||
const theme = createTheme();
|
||||
const styles = getLogRowStyles(theme);
|
||||
const labels = {
|
||||
place: 'Earth',
|
||||
planet: 'Mars',
|
||||
};
|
||||
const props: Props = {
|
||||
wrapLogMessage: false,
|
||||
row: createLogRow({ entry: 'Logs are wonderful', logLevel: LogLevel.error, timeEpochMs: 1546297200000, labels }),
|
||||
onOpenContext: () => {},
|
||||
styles,
|
||||
detectedFields,
|
||||
...propOverrides,
|
||||
};
|
||||
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<LogRowMessageDisplayedFields {...props} />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
return props;
|
||||
};
|
||||
|
||||
describe('LogRowMessageDisplayedFields', () => {
|
||||
it('renders diplayed fields from a log row', () => {
|
||||
setup();
|
||||
expect(screen.queryByText('Logs are wonderful')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/place=Earth/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/planet=Mars/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,51 +1,67 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { LogRowModel, Field, LinkModel, DataFrame } from '@grafana/data';
|
||||
import { withTheme2, Themeable2 } from '@grafana/ui';
|
||||
|
||||
import { LogRowMenuCell } from './LogRowMenuCell';
|
||||
import { LogRowStyles } from './getLogRowStyles';
|
||||
import { getAllFields } from './logParser';
|
||||
|
||||
export interface Props extends Themeable2 {
|
||||
export interface Props {
|
||||
row: LogRowModel;
|
||||
showDetectedFields: string[];
|
||||
detectedFields: string[];
|
||||
wrapLogMessage: boolean;
|
||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
||||
styles: LogRowStyles;
|
||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||
onOpenContext: (row: LogRowModel) => void;
|
||||
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
onUnpinLine?: (row: LogRowModel) => void;
|
||||
pinned?: boolean;
|
||||
}
|
||||
|
||||
class UnThemedLogRowMessageDisplayedFields extends PureComponent<Props> {
|
||||
render() {
|
||||
const { row, showDetectedFields, getFieldLinks, wrapLogMessage } = this.props;
|
||||
const fields = getAllFields(row, getFieldLinks);
|
||||
const wrapClassName = wrapLogMessage
|
||||
? ''
|
||||
: css`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
// only single key/value rows are filterable, so we only need the first field key for filtering
|
||||
const line = showDetectedFields
|
||||
.map((parsedKey) => {
|
||||
const field = fields.find((field) => {
|
||||
const { keys } = field;
|
||||
return keys[0] === parsedKey;
|
||||
});
|
||||
export const LogRowMessageDisplayedFields = React.memo((props: Props) => {
|
||||
const { row, detectedFields, getFieldLinks, wrapLogMessage, styles, ...rest } = props;
|
||||
const fields = getAllFields(row, getFieldLinks);
|
||||
const wrapClassName = wrapLogMessage ? '' : displayedFieldsStyles.noWrap;
|
||||
// only single key/value rows are filterable, so we only need the first field key for filtering
|
||||
const line = detectedFields
|
||||
.map((parsedKey) => {
|
||||
const field = fields.find((field) => {
|
||||
const { keys } = field;
|
||||
return keys[0] === parsedKey;
|
||||
});
|
||||
|
||||
if (field !== undefined && field !== null) {
|
||||
return `${parsedKey}=${field.values}`;
|
||||
}
|
||||
if (field !== undefined && field !== null) {
|
||||
return `${parsedKey}=${field.values}`;
|
||||
}
|
||||
|
||||
if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) {
|
||||
return `${parsedKey}=${row.labels[parsedKey]}`;
|
||||
}
|
||||
if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) {
|
||||
return `${parsedKey}=${row.labels[parsedKey]}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((s) => s !== null)
|
||||
.join(' ');
|
||||
return null;
|
||||
})
|
||||
.filter((s) => s !== null)
|
||||
.join(' ');
|
||||
|
||||
return <td className={wrapClassName}>{line}</td>;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<td className={styles.logsRowMessage}>
|
||||
<div className={wrapClassName}>{line}</div>
|
||||
</td>
|
||||
<td className={`log-row-menu-cell ${styles.logRowMenuCell}`}>
|
||||
<LogRowMenuCell logText={line} row={row} styles={styles} {...rest} />
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const displayedFieldsStyles = {
|
||||
noWrap: css`
|
||||
white-space: nowrap;
|
||||
`,
|
||||
};
|
||||
|
||||
export const LogRowMessageDisplayedFields = withTheme2(UnThemedLogRowMessageDisplayedFields);
|
||||
LogRowMessageDisplayedFields.displayName = 'LogRowMessageDisplayedFields';
|
||||
|
Loading…
Reference in New Issue
Block a user