Commit e7ae220c by David Committed by GitHub

Logs: Optional logs label column (#21025)

* Logs: Optional logs label column

- reintroduces label column that was removed when log details were
introduced
- added to explore and also as a new option to logs panel
- explore column settings now stored in localstorage
- labels are rendered with font size xs
- labels that start with `_` or are called `level` or `filename` are not
displayed
- removed click handlers and moved remaining `LogLabel` logic into `LogLabels`

* Added prop to satisfy interface

* Review feedback

* removed comment

* Changed label of label column switch
parent c1b74bec
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<Props, State> {
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 (
<span className={cx([styles.logsLabel])}>
<span className={cx([styles.logsLabelValue])} title={tooltip}>
{value}
</span>
{!plain && (
<span
title="Filter for label"
onClick={this.onClickLabel}
className={cx([styles.logsLabelIcon, 'fa fa-search-plus'])}
/>
)}
{!plain && getRows && (
<span onClick={this.onClickStats} className={cx([styles.logsLabelIcon, 'fa fa-signal'])} />
)}
{showStats && (
<span className={cx([styles.logsLabelStats])}>
<LogLabelStats stats={stats} rowCount={getRows().length} label={label} value={value} isLabel={true} />
</span>
)}
</span>
);
}
}
export const LogLabel = withTheme(UnThemedLogLabel);
LogLabel.displayName = 'LogLabel';
import React from 'react';
import { shallow } from 'enzyme';
import { UnThemedLogLabels as LogLabels } from './LogLabels';
import { getTheme } from '../../themes';
describe('<LogLabels />', () => {
it('renders notice when no labels are found', () => {
const wrapper = shallow(<LogLabels labels={{}} theme={getTheme()} />);
expect(wrapper.text()).toContain('no unique labels');
});
it('renders labels', () => {
const wrapper = shallow(<LogLabels labels={{ foo: 'bar', baz: '42' }} theme={getTheme()} />);
expect(wrapper.text()).toContain('bar');
expect(wrapper.text()).toContain('42');
});
it('exlcudes labels with certain names or labels starting with underscore', () => {
const wrapper = shallow(<LogLabels labels={{ foo: 'bar', level: '42', _private: '13' }} theme={getTheme()} />);
expect(wrapper.text()).toContain('bar');
expect(wrapper.text()).not.toContain('42');
expect(wrapper.text()).not.toContain('13');
});
});
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<Props> = ({ getRows, labels, onClickLabel, plain }) => {
const styles = getStyles();
export const UnThemedLogLabels: FunctionComponent<Props> = ({ labels, theme }) => {
const styles = getStyles(theme);
const displayLabels = Object.keys(labels).filter(label => !label.startsWith('_') && !HIDDEN_LABELS.includes(label));
if (displayLabels.length === 0) {
return (
<span className={cx([styles.logsLabels])}>
<span className={cx([styles.logsLabel])}>(no unique labels)</span>
</span>
);
}
return (
<span className={cx([styles.logsLabels])}>
{Object.keys(labels).map(key => (
<LogLabel
key={key}
getRows={getRows}
label={key}
value={labels[key]}
plain={plain}
onClickLabel={onClickLabel}
/>
))}
{displayLabels.map(label => {
const value = labels[label];
const tooltip = `${label}: ${value}`;
return (
<span key={label} className={cx([styles.logsLabel])}>
<span className={cx([styles.logsLabelValue])} title={tooltip}>
{value}
</span>
</span>
);
})}
</span>
);
};
export const LogLabels = withTheme(UnThemedLogLabels);
LogLabels.displayName = 'LogLabels';
......@@ -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<Props, State> {
row,
showDuplicates,
timeZone,
showLabels,
showTime,
wrapLogMessage,
theme,
......@@ -135,6 +138,11 @@ class UnThemedLogRow extends PureComponent<Props, State> {
{row.timeLocal}
</div>
)}
{showLabels && row.uniqueLabels && (
<div className={style.logsRowLabels}>
<LogLabels labels={row.uniqueLabels} />
</div>
)}
<LogRowMessage
highlighterExpressions={highlighterExpressions}
row={row}
......
......@@ -13,6 +13,7 @@ describe('LogRows', () => {
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'}
......
......@@ -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<Props, State> {
render() {
const {
dedupStrategy,
showLabels,
showTime,
wrapLogMessage,
logRows,
......@@ -117,6 +119,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
highlighterExpressions={highlighterExpressions}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
......@@ -135,6 +138,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
getRowContext={getRowContext}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
......
......@@ -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;
......
......@@ -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;
}
......
......@@ -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 (
<span className="logs-meta-item__labels">
<LogLabels labels={value} plain getRows={() => []} />
<LogLabels labels={value} />
</span>
);
}
......@@ -56,14 +63,16 @@ interface Props {
}
interface State {
showLabels: boolean;
showTime: boolean;
wrapLogMessage: boolean;
}
export class Logs extends PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
<div className="logs-panel-options">
<div className="logs-panel-controls">
<Switch label="Time" checked={showTime} onChange={this.onChangeTime} transparent />
<Switch label="Unique labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
<Switch label="Wrap lines" checked={wrapLogMessage} onChange={this.onChangewrapLogMessage} transparent />
<ToggleButtonGroup label="Dedup" transparent={true}>
{Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
......@@ -213,6 +238,7 @@ export class Logs extends PureComponent<Props, State> {
rowLimit={logRows ? logRows.length : undefined}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
......
......@@ -10,7 +10,7 @@ interface LogsPanelProps extends PanelProps<Options> {}
export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
data,
timeZone,
options: { showTime, wrapLogMessage, sortOrder },
options: { showLabels, showTime, wrapLogMessage, sortOrder },
width,
}) => {
if (!data) {
......@@ -30,6 +30,7 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
logRows={sortedNewResults.rows}
dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
......
......@@ -13,6 +13,13 @@ const sortOrderOptions = [
];
export class LogsPanelEditor extends PureComponent<PanelEditorProps<Options>> {
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<PanelEditorProps<Options>> {
};
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<PanelEditorProps<Options>> {
<PanelOptionsGrid>
<PanelOptionsGroup title="Columns">
<Switch label="Time" labelClass="width-10" checked={showTime} onChange={this.onToggleTime} />
<Switch label="Labels" labelClass="width-10" checked={showLabels} onChange={this.onToggleLabels} />
<Switch
label="Wrap lines"
labelClass="width-10"
......
import { SortOrder } from 'app/core/utils/explore';
export interface Options {
showLabels: boolean;
showTime: boolean;
wrapLogMessage: boolean;
sortOrder: SortOrder;
}
export const defaults: Options = {
showLabels: false,
showTime: true,
wrapLogMessage: true,
sortOrder: SortOrder.Descending,
......
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