Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logs: Display log row menu cell on displayed fields #71300

Merged
merged 10 commits into from Jul 11, 2023
9 changes: 8 additions & 1 deletion public/app/features/logs/components/LogRow.tsx
Expand Up @@ -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
Expand Down
122 changes: 122 additions & 0 deletions public/app/features/logs/components/LogRowMenuCell.tsx
@@ -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';
205 changes: 61 additions & 144 deletions public/app/features/logs/components/LogRowMessage.tsx
@@ -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;
Expand All @@ -25,33 +23,34 @@ 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 (
<Highlighter
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);
Expand All @@ -60,133 +59,51 @@ const restructureLog = memoizeOne((line: string, prettifyLogMessage: boolean): s
}
}
return line;
});

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;
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>
</>
);
});

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';