mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 08:35:43 -06:00
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
This commit is contained in:
parent
c1b74becce
commit
e7ae220cde
@ -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<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';
|
23
packages/grafana-ui/src/components/Logs/LogLabels.test.tsx
Normal file
23
packages/grafana-ui/src/components/Logs/LogLabels.test.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
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');
|
||||
});
|
||||
});
|
@ -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<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"
|
||||
|
@ -1,12 +1,14 @@
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user