From 842dde3dc965aea1d292a479e5ff4234bdbd243b Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Mon, 16 Dec 2019 16:07:36 +0100 Subject: [PATCH] Explore: Refactor log rows (#21066) --- .../src/components/Logs/LogDetails.test.tsx | 1 + .../src/components/Logs/LogDetails.tsx | 165 ++++++++++++------ .../src/components/Logs/LogDetailsRow.tsx | 6 +- .../src/components/Logs/LogLabels.tsx | 2 +- .../grafana-ui/src/components/Logs/LogRow.tsx | 142 +++++++++------ .../src/components/Logs/LogRowMessage.tsx | 4 +- .../src/components/Logs/LogRows.tsx | 92 +++++----- .../src/components/Logs/getLogRowStyles.ts | 45 ++--- public/app/features/explore/LiveLogs.tsx | 70 ++++---- 9 files changed, 300 insertions(+), 227 deletions(-) diff --git a/packages/grafana-ui/src/components/Logs/LogDetails.test.tsx b/packages/grafana-ui/src/components/Logs/LogDetails.test.tsx index c0b498131e1..54044b46ac1 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetails.test.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetails.test.tsx @@ -7,6 +7,7 @@ import { LogDetailsRow } from './LogDetailsRow'; const setup = (propOverrides?: Partial, rowOverrides?: Partial) => { const props: Props = { theme: {} as GrafanaTheme, + showDuplicates: false, row: { dataFrame: new MutableDataFrame(), entryFieldIndex: 0, diff --git a/packages/grafana-ui/src/components/Logs/LogDetails.tsx b/packages/grafana-ui/src/components/Logs/LogDetails.tsx index 14449640df8..791edf0b09c 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetails.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetails.tsx @@ -1,5 +1,6 @@ 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>; } +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 { getParser = memoizeOne(getParser); @@ -102,72 +130,93 @@ class UnThemedLogDetails extends PureComponent { }; 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 ( -
- - - {labelsAvailable && ( - - - - )} - {Object.keys(labels).map(key => { - const value = labels[key]; - return ( - calculateLogsLabelStats(getRows(), key)} - onClickFilterOutLabel={onClickFilterOutLabel} - onClickFilterLabel={onClickFilterLabel} - /> - ); - })} + + {showDuplicates && + + + )} + {fields.map(field => { + const { key, value, links, fieldIndex } = field; + return ( + + fieldIndex === undefined + ? this.getStatsForParsedField(key) + : calculateStats(row.dataFrame.fields[fieldIndex].values.toArray()) + } + /> + ); + })} + {!parsedFieldsAvailable && !labelsAvailable && ( + + + + )} + +
- Log Labels: -
} + + +
+ + + {labelsAvailable && ( + + + + )} + {Object.keys(labels).map(key => { + const value = labels[key]; + return ( + calculateLogsLabelStats(getRows(), key)} + onClickFilterOutLabel={onClickFilterOutLabel} + onClickFilterLabel={onClickFilterLabel} + /> + ); + })} - {parsedFieldsAvailable && ( - - - - )} - {fields.map(field => { - const { key, value, links, fieldIndex } = field; - return ( - - fieldIndex === undefined - ? this.getStatsForParsedField(key) - : calculateStats(row.dataFrame.fields[fieldIndex].values.toArray()) - } - /> - ); - })} - {!parsedFieldsAvailable && !labelsAvailable && ( - - - - )} - -
+ Log Labels: +
- Parsed Fields: -
- No details available -
-
+ {parsedFieldsAvailable && ( +
+ Parsed Fields: +
+ No details available +
+
+ + ); } } diff --git a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx index 0636a9600df..50b81e17514 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx @@ -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 { {/* Key - value columns */} {parsedKey} - + {parsedValue} {links && links.map(link => { diff --git a/packages/grafana-ui/src/components/Logs/LogLabels.tsx b/packages/grafana-ui/src/components/Logs/LogLabels.tsx index e89cf0cfb2d..cf763c08c87 100644 --- a/packages/grafana-ui/src/components/Logs/LogLabels.tsx +++ b/packages/grafana-ui/src/components/Logs/LogLabels.tsx @@ -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; diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index bd6dffa8df0..504b5b03f9b 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -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 { state: State = { showContext: false, showDetails: false, + hasHoverBackground: false, }; toggleContext = () => { @@ -69,6 +77,22 @@ class UnThemedLogRow extends PureComponent { }); }; + /** + * 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 { 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 ( -
- {showDuplicates && ( -
- {row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null} -
- )} -
- {!allowDetails && ( -
- -
- )} -
-
- {showTime && showUtc && ( -
- {row.timeUtc} -
- )} - {showTime && !showUtc && ( -
- {row.timeLocal} -
- )} - {showLabels && row.uniqueLabels && ( -
- -
- )} - -
- {this.state.showDetails && ( - + <> + + {showDuplicates && ( + + {row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null} + )} -
-
+ + {!allowDetails && ( + + + + )} + {showTime && showUtc && ( + + {row.timeUtc} + + )} + {showTime && !showUtc && ( + + {row.timeLocal} + + )} + {showLabels && row.uniqueLabels && ( + + + + )} + + + {this.state.showDetails && ( + + )} + ); } diff --git a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx index bda60e6a0b9..c54c641511b 100644 --- a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx @@ -96,7 +96,7 @@ class UnThemedLogRowMessage extends PureComponent { : cx([style.logsRowMatchHighLight]); const styles = getStyles(theme); return ( -
+
{showContext && context && ( { )}
-
+ ); } } diff --git a/packages/grafana-ui/src/components/Logs/LogRows.tsx b/packages/grafana-ui/src/components/Logs/LogRows.tsx index 40ccaf6e68d..f01116b073b 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.tsx @@ -88,7 +88,7 @@ class UnThemedLogRows extends PureComponent { 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 { const getRowContext = this.props.getRowContext ? this.props.getRowContext : () => Promise.resolve([]); return ( -
-
- {hasData && - firstRows.map((row, index) => ( - - ))} - {hasData && - renderAll && - lastRows.map((row, index) => ( - - ))} - {hasData && !renderAll && Rendering {rowCount - previewLimit!} rows...} -
+
+ + + {hasData && + firstRows.map((row, index) => ( + + ))} + {hasData && + renderAll && + lastRows.map((row, index) => ( + + ))} + {hasData && !renderAll && ( + + + + )} + +
Rendering {rowCount - previewLimit!} rows...
); } diff --git a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts index 2fce1717d70..803c48fff5c 100644 --- a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts +++ b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts @@ -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; diff --git a/public/app/features/explore/LiveLogs.tsx b/public/app/features/explore/LiveLogs.tsx index d5654551677..d1dcb0f5b06 100644 --- a/public/app/features/explore/LiveLogs.tsx +++ b/public/app/features/explore/LiveLogs.tsx @@ -64,7 +64,7 @@ interface State { class LiveLogs extends PureComponent { private liveEndDiv: HTMLDivElement | null = null; - private scrollContainerRef = React.createRef(); + private scrollContainerRef = React.createRef(); private lastScrollPos: number | null = null; constructor(props: Props) { @@ -139,39 +139,41 @@ class LiveLogs extends PureComponent { return (
-
- {this.rowsToRender().map((row: LogRowModel) => { - return ( -
- {showUtc && ( -
- {row.timeUtc} -
- )} - {!showUtc && ( -
- {row.timeLocal} -
- )} -
{row.entry}
-
- ); - })} -
{ - 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); - } - }} - /> -
+ + + {this.rowsToRender().map((row: LogRowModel) => { + return ( + + {showUtc && ( + + )} + {!showUtc && ( + + )} + + + ); + })} + { + 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); + } + }} + /> + +
+ {row.timeUtc} + + {row.timeLocal} + {row.entry}