Commit 842dde3d by Ivana Huckova Committed by GitHub

Explore: Refactor log rows (#21066)

parent 71382ae7
......@@ -7,6 +7,7 @@ import { LogDetailsRow } from './LogDetailsRow';
const setup = (propOverrides?: Partial<Props>, rowOverrides?: Partial<LogRowModel>) => {
const props: Props = {
theme: {} as GrafanaTheme,
showDuplicates: false,
row: {
dataFrame: new MutableDataFrame(),
entryFieldIndex: 0,
......
import React, { PureComponent } from 'react';
import memoizeOne from 'memoize-one';
import { css, cx } from 'emotion';
import {
calculateFieldStats,
calculateLogsLabelStats,
......@@ -8,11 +9,14 @@ import {
getParser,
LinkModel,
LogRowModel,
GrafanaTheme,
} from '@grafana/data';
import { Themeable } from '../../types/theme';
import { withTheme } from '../../themes/index';
import { getLogRowStyles } from './getLogRowStyles';
import { stylesFactory } from '../../themes/stylesFactory';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
//Components
import { LogDetailsRow } from './LogDetailsRow';
......@@ -26,12 +30,36 @@ type FieldDef = {
export interface Props extends Themeable {
row: LogRowModel;
showDuplicates: boolean;
getRows: () => LogRowModel[];
className?: string;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void;
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
}
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const bgColor = selectThemeVariant({ light: theme.colors.gray7, dark: theme.colors.dark2 }, theme.type);
return {
hoverBackground: css`
label: hoverBackground;
background-color: ${bgColor};
`,
logsRowLevelDetails: css`
label: logs-row__level_details;
&::after {
top: -3px;
}
`,
logDetailsDefaultCursor: css`
label: logDetailsDefaultCursor;
cursor: default;
`,
};
});
class UnThemedLogDetails extends PureComponent<Props> {
getParser = memoizeOne(getParser);
......@@ -102,72 +130,93 @@ class UnThemedLogDetails extends PureComponent<Props> {
};
render() {
const { row, theme, onClickFilterOutLabel, onClickFilterLabel, getRows } = this.props;
const {
row,
theme,
onClickFilterOutLabel,
onClickFilterLabel,
getRows,
showDuplicates,
className,
onMouseEnter,
onMouseLeave,
} = this.props;
const style = getLogRowStyles(theme, row.logLevel);
const styles = getStyles(theme);
const labels = row.labels ? row.labels : {};
const labelsAvailable = Object.keys(labels).length > 0;
const fields = this.getAllFields(row);
const parsedFieldsAvailable = fields && fields.length > 0;
return (
<div className={style.logDetailsContainer}>
<table className={style.logDetailsTable}>
<tbody>
{labelsAvailable && (
<tr>
<td colSpan={5} className={style.logDetailsHeading} aria-label="Log Labels">
Log Labels:
</td>
</tr>
)}
{Object.keys(labels).map(key => {
const value = labels[key];
return (
<LogDetailsRow
key={`${key}=${value}`}
parsedKey={key}
parsedValue={value}
isLabel={true}
getStats={() => calculateLogsLabelStats(getRows(), key)}
onClickFilterOutLabel={onClickFilterOutLabel}
onClickFilterLabel={onClickFilterLabel}
/>
);
})}
<tr
className={cx(className, styles.logDetailsDefaultCursor)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{showDuplicates && <td />}
<td className={cx(style.logsRowLevel, styles.logsRowLevelDetails)} />
<td colSpan={4}>
<div className={style.logDetailsContainer}>
<table className={style.logDetailsTable}>
<tbody>
{labelsAvailable && (
<tr>
<td colSpan={5} className={style.logDetailsHeading} aria-label="Log Labels">
Log Labels:
</td>
</tr>
)}
{Object.keys(labels).map(key => {
const value = labels[key];
return (
<LogDetailsRow
key={`${key}=${value}`}
parsedKey={key}
parsedValue={value}
isLabel={true}
getStats={() => calculateLogsLabelStats(getRows(), key)}
onClickFilterOutLabel={onClickFilterOutLabel}
onClickFilterLabel={onClickFilterLabel}
/>
);
})}
{parsedFieldsAvailable && (
<tr>
<td colSpan={5} className={style.logDetailsHeading} aria-label="Parsed Fields">
Parsed Fields:
</td>
</tr>
)}
{fields.map(field => {
const { key, value, links, fieldIndex } = field;
return (
<LogDetailsRow
key={`${key}=${value}`}
parsedKey={key}
parsedValue={value}
links={links}
getStats={() =>
fieldIndex === undefined
? this.getStatsForParsedField(key)
: calculateStats(row.dataFrame.fields[fieldIndex].values.toArray())
}
/>
);
})}
{!parsedFieldsAvailable && !labelsAvailable && (
<tr>
<td colSpan={5} aria-label="No details">
No details available
</td>
</tr>
)}
</tbody>
</table>
</div>
{parsedFieldsAvailable && (
<tr>
<td colSpan={5} className={style.logDetailsHeading} aria-label="Parsed Fields">
Parsed Fields:
</td>
</tr>
)}
{fields.map(field => {
const { key, value, links, fieldIndex } = field;
return (
<LogDetailsRow
key={`${key}=${value}`}
parsedKey={key}
parsedValue={value}
links={links}
getStats={() =>
fieldIndex === undefined
? this.getStatsForParsedField(key)
: calculateStats(row.dataFrame.fields[fieldIndex].values.toArray())
}
/>
);
})}
{!parsedFieldsAvailable && !labelsAvailable && (
<tr>
<td colSpan={5} aria-label="No details">
No details available
</td>
</tr>
)}
</tbody>
</table>
</div>
</td>
</tr>
);
}
}
......
......@@ -38,6 +38,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
label: hoverCursor;
cursor: pointer;
`,
wordBreakAll: css`
label: wordBreakAll;
word-break: break-all;
`,
};
});
......@@ -102,7 +106,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
{/* Key - value columns */}
<td className={style.logDetailsLabel}>{parsedKey}</td>
<td className={style.logsRowCell}>
<td className={styles.wordBreakAll}>
{parsedValue}
{links &&
links.map(link => {
......
......@@ -24,7 +24,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
padding: 0 2px;
background-color: ${selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark6 }, theme.type)};
border-radius: ${theme.border.radius};
margin: 0 4px 2px 0;
margin: 1px 4px 0 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
......
......@@ -12,6 +12,7 @@ import { Themeable } from '../../types/theme';
import { withTheme } from '../../themes/index';
import { getLogRowStyles } from './getLogRowStyles';
import { stylesFactory } from '../../themes/stylesFactory';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
//Components
import { LogDetails } from './LogDetails';
......@@ -38,14 +39,20 @@ interface Props extends Themeable {
interface State {
showContext: boolean;
showDetails: boolean;
hasHoverBackground: boolean;
}
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const bgColor = selectThemeVariant({ light: theme.colors.gray7, dark: theme.colors.dark2 }, theme.type);
return {
topVerticalAlign: css`
label: topVerticalAlign;
vertical-align: top;
`,
hoverBackground: css`
label: hoverBackground;
background-color: ${bgColor};
`,
};
});
/**
......@@ -59,6 +66,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
state: State = {
showContext: false,
showDetails: false,
hasHoverBackground: false,
};
toggleContext = () => {
......@@ -69,6 +77,22 @@ class UnThemedLogRow extends PureComponent<Props, State> {
});
};
/**
* We are using onMouse events to change background of Log Details Table to hover-state-background when
* hovered over Log Row and vice versa. This can't be done with css because we use 2 separate table rows without common parent element.
*/
addHoverBackground = () => {
this.setState({
hasHoverBackground: true,
});
};
clearHoverBackground = () => {
this.setState({
hasHoverBackground: false,
});
};
toggleDetails = () => {
if (this.props.allowDetails) {
return;
......@@ -101,72 +125,76 @@ class UnThemedLogRow extends PureComponent<Props, State> {
theme,
getFieldLinks,
} = this.props;
const { showDetails, showContext } = this.state;
const { showDetails, showContext, hasHoverBackground } = this.state;
const style = getLogRowStyles(theme, row.logLevel);
const styles = getStyles(theme);
const showUtc = timeZone === 'utc';
const showDetailsClassName = showDetails
? cx(['fa fa-chevron-down', styles.topVerticalAlign])
: cx(['fa fa-chevron-right', styles.topVerticalAlign]);
const hoverBackground = cx(style.logsRow, { [styles.hoverBackground]: hasHoverBackground });
return (
<div className={style.logsRow}>
{showDuplicates && (
<div className={style.logsRowDuplicates}>
{row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
</div>
)}
<div className={style.logsRowLevel} />
{!allowDetails && (
<div
title={showDetails ? 'Hide log details' : 'See log details'}
onClick={this.toggleDetails}
className={style.logsRowToggleDetails}
>
<i className={showDetailsClassName} />
</div>
)}
<div>
<div onClick={this.toggleDetails}>
{showTime && showUtc && (
<div className={style.logsRowLocalTime} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timeUtc}
</div>
)}
{showTime && !showUtc && (
<div className={style.logsRowLocalTime} title={`${row.timeUtc} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
{showLabels && row.uniqueLabels && (
<div className={style.logsRowLabels}>
<LogLabels labels={row.uniqueLabels} />
</div>
)}
<LogRowMessage
highlighterExpressions={highlighterExpressions}
row={row}
getRows={getRows}
errors={errors}
hasMoreContextRows={hasMoreContextRows}
updateLimit={updateLimit}
context={context}
showContext={showContext}
wrapLogMessage={wrapLogMessage}
onToggleContext={this.toggleContext}
/>
</div>
{this.state.showDetails && (
<LogDetails
getFieldLinks={getFieldLinks}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
getRows={getRows}
row={row}
/>
<>
<tr
className={hoverBackground}
onMouseEnter={this.addHoverBackground}
onMouseLeave={this.clearHoverBackground}
onClick={this.toggleDetails}
>
{showDuplicates && (
<td className={style.logsRowDuplicates}>
{row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
</td>
)}
<td className={style.logsRowLevel} />
{!allowDetails && (
<td title={showDetails ? 'Hide log details' : 'See log details'} className={style.logsRowToggleDetails}>
<i className={showDetailsClassName} />
</td>
)}
{showTime && showUtc && (
<td className={style.logsRowLocalTime} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timeUtc}
</td>
)}
</div>
</div>
{showTime && !showUtc && (
<td className={style.logsRowLocalTime} title={`${row.timeUtc} (${row.timeFromNow})`}>
{row.timeLocal}
</td>
)}
{showLabels && row.uniqueLabels && (
<td className={style.logsRowLabels}>
<LogLabels labels={row.uniqueLabels} />
</td>
)}
<LogRowMessage
highlighterExpressions={highlighterExpressions}
row={row}
getRows={getRows}
errors={errors}
hasMoreContextRows={hasMoreContextRows}
updateLimit={updateLimit}
context={context}
showContext={showContext}
wrapLogMessage={wrapLogMessage}
onToggleContext={this.toggleContext}
/>
</tr>
{this.state.showDetails && (
<LogDetails
className={hoverBackground}
onMouseEnter={this.addHoverBackground}
onMouseLeave={this.clearHoverBackground}
showDuplicates={showDuplicates}
getFieldLinks={getFieldLinks}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
getRows={getRows}
row={row}
/>
)}
</>
);
}
......
......@@ -96,7 +96,7 @@ class UnThemedLogRowMessage extends PureComponent<Props, State> {
: cx([style.logsRowMatchHighLight]);
const styles = getStyles(theme);
return (
<div className={style.logsRowMessage}>
<td className={style.logsRowMessage}>
<div className={cx(styles.positionRelative, { [styles.horizontalScroll]: !wrapLogMessage })}>
{showContext && context && (
<LogRowContext
......@@ -133,7 +133,7 @@ class UnThemedLogRowMessage extends PureComponent<Props, State> {
</span>
)}
</div>
</div>
</td>
);
}
}
......
......@@ -88,7 +88,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
getFieldLinks,
} = this.props;
const { renderAll } = this.state;
const { logsRows, logsRowsHorizontalScroll } = getLogRowStyles(theme);
const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme);
const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows;
const hasData = logRows && logRows.length > 0;
const dedupCount = dedupedRows
......@@ -108,48 +108,54 @@ class UnThemedLogRows extends PureComponent<Props, State> {
const getRowContext = this.props.getRowContext ? this.props.getRowContext : () => Promise.resolve([]);
return (
<div className={logsRows}>
<div className={horizontalScrollWindow}>
{hasData &&
firstRows.map((row, index) => (
<LogRow
key={row.uid}
getRows={getRows}
getRowContext={getRowContext}
highlighterExpressions={highlighterExpressions}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
allowDetails={allowDetails}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
getFieldLinks={getFieldLinks}
/>
))}
{hasData &&
renderAll &&
lastRows.map((row, index) => (
<LogRow
key={row.uid}
getRows={getRows}
getRowContext={getRowContext}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
allowDetails={allowDetails}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
getFieldLinks={getFieldLinks}
/>
))}
{hasData && !renderAll && <span>Rendering {rowCount - previewLimit!} rows...</span>}
</div>
<div className={horizontalScrollWindow}>
<table className={logsRowsTable}>
<tbody>
{hasData &&
firstRows.map((row, index) => (
<LogRow
key={row.uid}
getRows={getRows}
getRowContext={getRowContext}
highlighterExpressions={highlighterExpressions}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
allowDetails={allowDetails}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
getFieldLinks={getFieldLinks}
/>
))}
{hasData &&
renderAll &&
lastRows.map((row, index) => (
<LogRow
key={row.uid}
getRows={getRows}
getRowContext={getRowContext}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
allowDetails={allowDetails}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
getFieldLinks={getFieldLinks}
/>
))}
{hasData && !renderAll && (
<tr>
<td colSpan={5}>Rendering {rowCount - previewLimit!} rows...</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
......
......@@ -53,23 +53,22 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
background-color: rgba(${theme.colors.yellow}, 0.2);
border-bottom-style: dotted;
`,
logsRows: css`
logsRowsTable: css`
label: logs-rows;
font-family: ${theme.typography.fontFamily.monospace};
font-size: ${theme.typography.size.sm};
display: table;
table-layout: fixed;
width: 100%;
`,
logsRowsHorizontalScroll: css`
label: logs-rows__horizontal-scroll;
overflow-y: scroll;
overflow: scroll;
`,
context: context,
logsRow: css`
label: logs-row;
display: table-row;
width: 100%;
cursor: pointer;
vertical-align: top;
&:hover {
.${context} {
visibility: visible;
......@@ -82,8 +81,7 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
}
}
> div {
display: table-cell;
> td {
padding-right: ${theme.spacing.sm};
border-top: ${theme.border.width.sm} solid transparent;
border-bottom: ${theme.border.width.sm} solid transparent;
......@@ -103,9 +101,7 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
logsRowLevel: css`
label: logs-row__level;
position: relative;
width: 10px;
cursor: default;
&::after {
content: '';
display: block;
......@@ -116,39 +112,24 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
background-color: ${logColor};
}
`,
logsRowCell: css`
label: logs-row-cell;
word-break: break-all;
padding-right: ${theme.spacing.sm};
`,
logsRowToggleDetails: css`
label: logs-row-toggle-details__level;
position: relative;
width: 15px;
font-size: 9px;
padding-top: 5px;
`,
logsRowLocalTime: css`
label: logs-row__localtime;
display: table-cell;
white-space: nowrap;
width: 12.5em;
padding-right: 1em;
`,
logsRowLabels: css`
label: logs-row__labels;
display: table-cell;
white-space: nowrap;
width: 22em;
padding-right: 1em;
max-width: 22em;
`,
logsRowMessage: css`
label: logs-row__message;
word-break: break-all;
display: table-cell;
`,
logsRowStats: css`
label: logs-row__stats;
margin: 5px 0;
`,
//Log details sepcific CSS
logDetailsContainer: css`
......@@ -156,23 +137,27 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
border: 1px solid ${borderColor};
padding: 0 ${theme.spacing.sm} ${theme.spacing.sm};
border-radius: 3px;
margin: 20px 0;
margin: 20px 8px 20px 16px;
cursor: default;
`,
logDetailsTable: css`
label: logs-row-details-table;
line-height: 2;
width: 100%;
td:last-child {
width: 100%;
}
`,
logsDetailsIcon: css`
label: logs-row-details__icon;
position: relative;
padding-right: ${theme.spacing.sm};
padding-right: ${theme.spacing.md};
color: ${theme.colors.gray3};
`,
logDetailsLabel: css`
label: logs-row-details__label;
max-width: 25em;
min-width: 12em;
min-width: 15em;
padding: 0 ${theme.spacing.sm};
word-break: break-all;
`,
......@@ -183,8 +168,6 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
`,
logDetailsValue: css`
label: logs-row-details__row;
line-height: 2;
padding: ${theme.spacing.sm};
position: relative;
vertical-align: top;
cursor: default;
......
......@@ -64,7 +64,7 @@ interface State {
class LiveLogs extends PureComponent<Props, State> {
private liveEndDiv: HTMLDivElement | null = null;
private scrollContainerRef = React.createRef<HTMLDivElement>();
private scrollContainerRef = React.createRef<HTMLTableSectionElement>();
private lastScrollPos: number | null = null;
constructor(props: Props) {
......@@ -139,39 +139,41 @@ class LiveLogs extends PureComponent<Props, State> {
return (
<div>
<div
onScroll={isPaused ? undefined : this.onScroll}
className={cx(['logs-rows', styles.logsRowsLive])}
ref={this.scrollContainerRef}
>
{this.rowsToRender().map((row: LogRowModel) => {
return (
<div className={cx(logsRow, styles.logsRowFade)} key={row.uid}>
{showUtc && (
<div className={cx(logsRowLocalTime)} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timeUtc}
</div>
)}
{!showUtc && (
<div className={cx(logsRowLocalTime)} title={`${row.timeUtc} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
<div className={cx(logsRowMessage)}>{row.entry}</div>
</div>
);
})}
<div
ref={element => {
this.liveEndDiv = element;
// This is triggered on every update so on every new row. It keeps the view scrolled at the bottom by
// default.
if (this.liveEndDiv && !isPaused) {
this.liveEndDiv.scrollIntoView(false);
}
}}
/>
</div>
<table>
<tbody
onScroll={isPaused ? undefined : this.onScroll}
className={cx(['logs-rows', styles.logsRowsLive])}
ref={this.scrollContainerRef}
>
{this.rowsToRender().map((row: LogRowModel) => {
return (
<tr className={cx(logsRow, styles.logsRowFade)} key={row.uid}>
{showUtc && (
<td className={cx(logsRowLocalTime)} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timeUtc}
</td>
)}
{!showUtc && (
<td className={cx(logsRowLocalTime)} title={`${row.timeUtc} (${row.timeFromNow})`}>
{row.timeLocal}
</td>
)}
<td className={cx(logsRowMessage)}>{row.entry}</td>
</tr>
);
})}
<tr
ref={element => {
this.liveEndDiv = element;
// This is triggered on every update so on every new row. It keeps the view scrolled at the bottom by
// default.
if (this.liveEndDiv && !isPaused) {
this.liveEndDiv.scrollIntoView(false);
}
}}
/>
</tbody>
</table>
<div className={cx([styles.logsRowsIndicator])}>
<button onClick={isPaused ? onResume : onPause} className={cx('btn btn-secondary', styles.button)}>
<i className={cx('fa', isPaused ? 'fa-play' : 'fa-pause')} />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment