diff --git a/packages/grafana-ui/src/components/Logs/LogLabel.tsx b/packages/grafana-ui/src/components/Logs/LogLabel.tsx deleted file mode 100644 index 9298b35f7a3..00000000000 --- a/packages/grafana-ui/src/components/Logs/LogLabel.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { PureComponent } from 'react'; -import { css, cx } from 'emotion'; -import { LogRowModel, LogLabelStatsModel, calculateLogsLabelStats } from '@grafana/data'; - -import { LogLabelStats } from './LogLabelStats'; -import { Themeable } from '../../types/theme'; -import { GrafanaTheme } from '@grafana/data'; -import { selectThemeVariant } from '../../themes/selectThemeVariant'; -import { withTheme } from '../../themes/ThemeContext'; -import { stylesFactory } from '../../themes'; - -const getStyles = stylesFactory((theme: GrafanaTheme) => { - return { - logsLabel: css` - label: logs-label; - display: flex; - 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; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - `, - logsLabelValue: css` - label: logs-label__value; - display: inline-block; - max-width: 20em; - text-overflow: ellipsis; - overflow: hidden; - `, - logsLabelIcon: css` - label: logs-label__icon; - border-left: solid 1px ${selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark1 }, theme.type)}; - padding: 0 2px; - cursor: pointer; - margin-left: 2px; - `, - logsLabelStats: css` - position: absolute; - top: 1.25em; - left: -10px; - z-index: 100; - justify-content: space-between; - box-shadow: 0 0 20px ${selectThemeVariant({ light: theme.colors.white, dark: theme.colors.black }, theme.type)}; - `, - }; -}); - -interface Props extends Themeable { - value: string; - label: string; - getRows: () => LogRowModel[]; - plain?: boolean; - onClickLabel?: (label: string, value: string) => void; -} - -interface State { - showStats: boolean; - stats: LogLabelStatsModel[]; -} - -class UnThemedLogLabel extends PureComponent { - state: State = { - stats: [], - showStats: false, - }; - - onClickClose = () => { - this.setState({ showStats: false }); - }; - - onClickLabel = () => { - const { onClickLabel, label, value } = this.props; - if (onClickLabel) { - onClickLabel(label, value); - } - }; - - onClickStats = () => { - this.setState(state => { - if (state.showStats) { - return { showStats: false, stats: [] }; - } - const allRows = this.props.getRows(); - const stats = calculateLogsLabelStats(allRows, this.props.label); - return { showStats: true, stats }; - }); - }; - - render() { - const { getRows, label, plain, value, theme } = this.props; - const styles = getStyles(theme); - const { showStats, stats } = this.state; - const tooltip = `${label}: ${value}`; - return ( - - - {value} - - {!plain && ( - - )} - {!plain && getRows && ( - - )} - {showStats && ( - - - - )} - - ); - } -} - -export const LogLabel = withTheme(UnThemedLogLabel); -LogLabel.displayName = 'LogLabel'; diff --git a/packages/grafana-ui/src/components/Logs/LogLabels.test.tsx b/packages/grafana-ui/src/components/Logs/LogLabels.test.tsx new file mode 100644 index 00000000000..3a38ed0f0d9 --- /dev/null +++ b/packages/grafana-ui/src/components/Logs/LogLabels.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UnThemedLogLabels as LogLabels } from './LogLabels'; +import { getTheme } from '../../themes'; + +describe('', () => { + it('renders notice when no labels are found', () => { + const wrapper = shallow(); + expect(wrapper.text()).toContain('no unique labels'); + }); + it('renders labels', () => { + const wrapper = shallow(); + expect(wrapper.text()).toContain('bar'); + expect(wrapper.text()).toContain('42'); + }); + it('exlcudes labels with certain names or labels starting with underscore', () => { + const wrapper = shallow(); + expect(wrapper.text()).toContain('bar'); + expect(wrapper.text()).not.toContain('42'); + expect(wrapper.text()).not.toContain('13'); + }); +}); diff --git a/packages/grafana-ui/src/components/Logs/LogLabels.tsx b/packages/grafana-ui/src/components/Logs/LogLabels.tsx index 25a455e5148..e89cf0cfb2d 100644 --- a/packages/grafana-ui/src/components/Logs/LogLabels.tsx +++ b/packages/grafana-ui/src/components/Logs/LogLabels.tsx @@ -1,41 +1,76 @@ import React, { FunctionComponent } from 'react'; import { css, cx } from 'emotion'; -import { Labels, LogRowModel } from '@grafana/data'; +import { Labels } from '@grafana/data'; -import { LogLabel } from './LogLabel'; import { stylesFactory } from '../../themes'; +import { Themeable } from '../../types/theme'; +import { GrafanaTheme } from '@grafana/data'; +import { selectThemeVariant } from '../../themes/selectThemeVariant'; +import { withTheme } from '../../themes/ThemeContext'; -const getStyles = stylesFactory(() => ({ - logsLabels: css` - display: flex; - flex-wrap: wrap; - `, -})); +// Levels are already encoded in color, filename is a Loki-ism +const HIDDEN_LABELS = ['level', 'lvl', 'filename']; -interface Props { +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + logsLabels: css` + display: flex; + flex-wrap: wrap; + font-size: ${theme.typography.size.xs}; + `, + logsLabel: css` + label: logs-label; + display: flex; + 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; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + `, + logsLabelValue: css` + label: logs-label__value; + display: inline-block; + max-width: 20em; + text-overflow: ellipsis; + overflow: hidden; + `, + }; +}); + +interface Props extends Themeable { labels: Labels; - getRows: () => LogRowModel[]; - plain?: boolean; - onClickLabel?: (label: string, value: string) => void; } -export const LogLabels: FunctionComponent = ({ getRows, labels, onClickLabel, plain }) => { - const styles = getStyles(); +export const UnThemedLogLabels: FunctionComponent = ({ labels, theme }) => { + const styles = getStyles(theme); + const displayLabels = Object.keys(labels).filter(label => !label.startsWith('_') && !HIDDEN_LABELS.includes(label)); + + if (displayLabels.length === 0) { + return ( + + (no unique labels) + + ); + } return ( - {Object.keys(labels).map(key => ( - - ))} + {displayLabels.map(label => { + const value = labels[label]; + const tooltip = `${label}: ${value}`; + return ( + + + {value} + + + ); + })} ); }; +export const LogLabels = withTheme(UnThemedLogLabels); LogLabels.displayName = 'LogLabels'; diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index 771ff78534a..bd6dffa8df0 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -16,11 +16,13 @@ import { stylesFactory } from '../../themes/stylesFactory'; //Components import { LogDetails } from './LogDetails'; import { LogRowMessage } from './LogRowMessage'; +import { LogLabels } from './LogLabels'; interface Props extends Themeable { highlighterExpressions?: string[]; row: LogRowModel; showDuplicates: boolean; + showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; timeZone: TimeZone; @@ -93,6 +95,7 @@ class UnThemedLogRow extends PureComponent { row, showDuplicates, timeZone, + showLabels, showTime, wrapLogMessage, theme, @@ -135,6 +138,11 @@ class UnThemedLogRow extends PureComponent { {row.timeLocal} )} + {showLabels && row.uniqueLabels && ( +
+ +
+ )} { logRows={rows} dedupStrategy={LogsDedupStrategy.none} highlighterExpressions={[]} + showLabels={false} showTime={false} wrapLogMessage={true} timeZone={'utc'} @@ -33,6 +34,7 @@ describe('LogRows', () => { logRows={rows} dedupStrategy={LogsDedupStrategy.none} highlighterExpressions={[]} + showLabels={false} showTime={false} wrapLogMessage={true} timeZone={'utc'} @@ -62,6 +64,7 @@ describe('LogRows', () => { deduplicatedRows={dedupedRows} dedupStrategy={LogsDedupStrategy.none} highlighterExpressions={[]} + showLabels={false} showTime={false} wrapLogMessage={true} timeZone={'utc'} @@ -81,6 +84,7 @@ describe('LogRows', () => { logRows={rows} dedupStrategy={LogsDedupStrategy.none} highlighterExpressions={[]} + showLabels={false} showTime={false} wrapLogMessage={true} timeZone={'utc'} diff --git a/packages/grafana-ui/src/components/Logs/LogRows.tsx b/packages/grafana-ui/src/components/Logs/LogRows.tsx index c7c55112049..40ccaf6e68d 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.tsx @@ -17,6 +17,7 @@ export interface Props extends Themeable { deduplicatedRows?: LogRowModel[]; dedupStrategy: LogsDedupStrategy; highlighterExpressions?: string[]; + showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; timeZone: TimeZone; @@ -71,6 +72,7 @@ class UnThemedLogRows extends PureComponent { render() { const { dedupStrategy, + showLabels, showTime, wrapLogMessage, logRows, @@ -117,6 +119,7 @@ class UnThemedLogRows extends PureComponent { highlighterExpressions={highlighterExpressions} row={row} showDuplicates={showDuplicates} + showLabels={showLabels} showTime={showTime} wrapLogMessage={wrapLogMessage} timeZone={timeZone} @@ -135,6 +138,7 @@ class UnThemedLogRows extends PureComponent { getRowContext={getRowContext} row={row} showDuplicates={showDuplicates} + showLabels={showLabels} showTime={showTime} wrapLogMessage={wrapLogMessage} timeZone={timeZone} diff --git a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts index 2463476b622..d19fbe804e9 100644 --- a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts +++ b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts @@ -136,6 +136,13 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo width: 12.5em; padding-right: 1em; `, + logsRowLabels: css` + label: logs-row__labels; + display: table-cell; + white-space: nowrap; + width: 22em; + padding-right: 1em; + `, logsRowMessage: css` label: logs-row__message; word-break: break-all; diff --git a/public/app/core/store.ts b/public/app/core/store.ts index b6d721d6d1a..e29111358c5 100644 --- a/public/app/core/store.ts +++ b/public/app/core/store.ts @@ -9,7 +9,7 @@ export class Store { window.localStorage[key] = value; } - getBool(key: string, def: any) { + getBool(key: string, def: boolean): boolean { if (def !== void 0 && !this.exists(key)) { return def; } diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 764f9766e6d..ba05a554850 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -16,14 +16,21 @@ import { Field, } from '@grafana/data'; import { Switch, LogLabels, ToggleButtonGroup, ToggleButton, LogRows } from '@grafana/ui'; +import store from 'app/core/store'; import { ExploreGraphPanel } from './ExploreGraphPanel'; +const SETTINGS_KEYS = { + showLabels: 'grafana.explore.logs.showLabels', + showTime: 'grafana.explore.logs.showTime', + wrapLogMessage: 'grafana.explore.logs.wrapLogMessage', +}; + function renderMetaItem(value: any, kind: LogsMetaKind) { if (kind === LogsMetaKind.LabelsMap) { return ( - []} /> + ); } @@ -56,14 +63,16 @@ interface Props { } interface State { + showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; } export class Logs extends PureComponent { state = { - showTime: true, - wrapLogMessage: true, + showLabels: store.getBool(SETTINGS_KEYS.showLabels, false), + showTime: store.getBool(SETTINGS_KEYS.showTime, true), + wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true), }; onChangeDedup = (dedup: LogsDedupStrategy) => { @@ -74,21 +83,36 @@ export class Logs extends PureComponent { return onDedupStrategyChange(dedup); }; + onChangeLabels = (event?: React.SyntheticEvent) => { + const target = event && (event.target as HTMLInputElement); + if (target) { + const showLabels = target.checked; + this.setState({ + showLabels, + }); + store.set(SETTINGS_KEYS.showLabels, showLabels); + } + }; + onChangeTime = (event?: React.SyntheticEvent) => { const target = event && (event.target as HTMLInputElement); if (target) { + const showTime = target.checked; this.setState({ - showTime: target.checked, + showTime, }); + store.set(SETTINGS_KEYS.showTime, showTime); } }; onChangewrapLogMessage = (event?: React.SyntheticEvent) => { const target = event && (event.target as HTMLInputElement); if (target) { + const wrapLogMessage = target.checked; this.setState({ - wrapLogMessage: target.checked, + wrapLogMessage, }); + store.set(SETTINGS_KEYS.wrapLogMessage, wrapLogMessage); } }; @@ -134,7 +158,7 @@ export class Logs extends PureComponent { return null; } - const { showTime, wrapLogMessage } = this.state; + const { showLabels, showTime, wrapLogMessage } = this.state; const { dedupStrategy } = this.props; const hasData = logRows && logRows.length > 0; const dedupCount = dedupedRows @@ -175,6 +199,7 @@ export class Logs extends PureComponent {
+ {Object.keys(LogsDedupStrategy).map((dedupType: string, i) => ( @@ -213,6 +238,7 @@ export class Logs extends PureComponent { rowLimit={logRows ? logRows.length : undefined} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} + showLabels={showLabels} showTime={showTime} wrapLogMessage={wrapLogMessage} timeZone={timeZone} diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index d9198ac4bc3..ab8d186eee5 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -10,7 +10,7 @@ interface LogsPanelProps extends PanelProps {} export const LogsPanel: React.FunctionComponent = ({ data, timeZone, - options: { showTime, wrapLogMessage, sortOrder }, + options: { showLabels, showTime, wrapLogMessage, sortOrder }, width, }) => { if (!data) { @@ -30,6 +30,7 @@ export const LogsPanel: React.FunctionComponent = ({ logRows={sortedNewResults.rows} dedupStrategy={LogsDedupStrategy.none} highlighterExpressions={[]} + showLabels={showLabels} showTime={showTime} wrapLogMessage={wrapLogMessage} timeZone={timeZone} diff --git a/public/app/plugins/panel/logs/LogsPanelEditor.tsx b/public/app/plugins/panel/logs/LogsPanelEditor.tsx index c27632e1cd6..a39af06d1fc 100644 --- a/public/app/plugins/panel/logs/LogsPanelEditor.tsx +++ b/public/app/plugins/panel/logs/LogsPanelEditor.tsx @@ -13,6 +13,13 @@ const sortOrderOptions = [ ]; export class LogsPanelEditor extends PureComponent> { + onToggleLabels = () => { + const { options, onOptionsChange } = this.props; + const { showLabels } = options; + + onOptionsChange({ ...options, showLabels: !showLabels }); + }; + onToggleTime = () => { const { options, onOptionsChange } = this.props; const { showTime } = options; @@ -33,7 +40,7 @@ export class LogsPanelEditor extends PureComponent> { }; render() { - const { showTime, wrapLogMessage, sortOrder } = this.props.options; + const { showLabels, showTime, wrapLogMessage, sortOrder } = this.props.options; const value = sortOrderOptions.filter(option => option.value === sortOrder)[0]; return ( @@ -41,6 +48,7 @@ export class LogsPanelEditor extends PureComponent> { +