mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Logs panel: Support details view (#34125)
* Show log details in Logs panel * Add hide log details as panel option * Refactor tests to use testing library * Change hideLogDetails to enableLogsDetails * Add enableLogDetails to test file
This commit is contained in:
parent
10a19ab790
commit
a40aef0822
@ -1,12 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
import { LogDetailsRow, Props } from './LogDetailsRow';
|
import { screen, render, fireEvent } from '@testing-library/react';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { LogDetailsRow } from './LogDetailsRow';
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { LogLabelStats } from './LogLabelStats';
|
type Props = ComponentProps<typeof LogDetailsRow>;
|
||||||
|
|
||||||
const setup = (propOverrides?: Partial<Props>) => {
|
const setup = (propOverrides?: Partial<Props>) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
theme: {} as GrafanaTheme,
|
|
||||||
parsedValue: '',
|
parsedValue: '',
|
||||||
parsedKey: '',
|
parsedKey: '',
|
||||||
isLabel: true,
|
isLabel: true,
|
||||||
@ -14,40 +13,67 @@ const setup = (propOverrides?: Partial<Props>) => {
|
|||||||
getStats: () => null,
|
getStats: () => null,
|
||||||
onClickFilterLabel: () => {},
|
onClickFilterLabel: () => {},
|
||||||
onClickFilterOutLabel: () => {},
|
onClickFilterOutLabel: () => {},
|
||||||
|
onClickShowDetectedField: () => {},
|
||||||
|
onClickHideDetectedField: () => {},
|
||||||
|
showDetectedFields: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
const wrapper = mount(<LogDetailsRow {...props} />);
|
return render(<LogDetailsRow {...props} />);
|
||||||
return wrapper;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('LogDetailsRow', () => {
|
describe('LogDetailsRow', () => {
|
||||||
it('should render parsed key', () => {
|
it('should render parsed key', () => {
|
||||||
const wrapper = setup({ parsedKey: 'test key' });
|
setup({ parsedKey: 'test key' });
|
||||||
expect(wrapper.text().includes('test key')).toBe(true);
|
expect(screen.getByText('test key')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('should render parsed value', () => {
|
it('should render parsed value', () => {
|
||||||
const wrapper = setup({ parsedValue: 'test value' });
|
setup({ parsedValue: 'test value' });
|
||||||
expect(wrapper.text().includes('test value')).toBe(true);
|
expect(screen.getByText('test value')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render metrics button', () => {
|
it('should render metrics button', () => {
|
||||||
const wrapper = setup();
|
setup();
|
||||||
expect(wrapper.find({ title: 'Ad-hoc statistics' }).hostNodes()).toHaveLength(1);
|
expect(screen.getAllByTitle('Ad-hoc statistics')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if props is a label', () => {
|
describe('if props is a label', () => {
|
||||||
it('should render filter label button', () => {
|
it('should render filter label button', () => {
|
||||||
const wrapper = setup();
|
setup();
|
||||||
expect(wrapper.find({ title: 'Filter for value' }).hostNodes()).toHaveLength(1);
|
expect(screen.getAllByTitle('Filter for value')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
it('should render filter out label button', () => {
|
it('should render filter out label button', () => {
|
||||||
const wrapper = setup();
|
setup();
|
||||||
expect(wrapper.find({ title: 'Filter out value' }).hostNodes()).toHaveLength(1);
|
expect(screen.getAllByTitle('Filter out value')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
it('should not render filtering buttons if no filtering functions provided', () => {
|
||||||
|
setup({ onClickFilterLabel: undefined, onClickFilterOutLabel: undefined });
|
||||||
|
expect(screen.queryByTitle('Filter out value')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if props is not a label', () => {
|
||||||
|
it('should not render a filter label button', () => {
|
||||||
|
setup({ isLabel: false });
|
||||||
|
expect(screen.queryByTitle('Filter for value')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should render a show toggleFieldButton button', () => {
|
||||||
|
setup({ isLabel: false });
|
||||||
|
expect(screen.getAllByTitle('Show this field instead of the message')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
it('should not render a show toggleFieldButton button if no detected fields toggling functions provided', () => {
|
||||||
|
setup({
|
||||||
|
isLabel: false,
|
||||||
|
onClickShowDetectedField: undefined,
|
||||||
|
onClickHideDetectedField: undefined,
|
||||||
|
});
|
||||||
|
expect(screen.queryByTitle('Show this field instead of the message')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render stats when stats icon is clicked', () => {
|
it('should render stats when stats icon is clicked', () => {
|
||||||
const wrapper = setup({
|
setup({
|
||||||
parsedKey: 'key',
|
parsedKey: 'key',
|
||||||
parsedValue: 'value',
|
parsedValue: 'value',
|
||||||
getStats: () => {
|
getStats: () => {
|
||||||
@ -66,9 +92,10 @@ describe('LogDetailsRow', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find(LogLabelStats).length).toBe(0);
|
expect(screen.queryByTestId('logLabelStats')).not.toBeInTheDocument();
|
||||||
wrapper.find({ title: 'Ad-hoc statistics' }).hostNodes().simulate('click');
|
const adHocStatsButton = screen.getByTitle('Ad-hoc statistics');
|
||||||
expect(wrapper.find(LogLabelStats).length).toBe(1);
|
fireEvent.click(adHocStatsButton);
|
||||||
expect(wrapper.find(LogLabelStats).contains('another value')).toBeTruthy();
|
expect(screen.getByTestId('logLabelStats')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('logLabelStats')).toHaveTextContent('another value');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -112,10 +112,26 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { theme, parsedKey, parsedValue, isLabel, links, showDetectedFields, wrapLogMessage } = this.props;
|
const {
|
||||||
|
theme,
|
||||||
|
parsedKey,
|
||||||
|
parsedValue,
|
||||||
|
isLabel,
|
||||||
|
links,
|
||||||
|
showDetectedFields,
|
||||||
|
wrapLogMessage,
|
||||||
|
onClickShowDetectedField,
|
||||||
|
onClickHideDetectedField,
|
||||||
|
onClickFilterLabel,
|
||||||
|
onClickFilterOutLabel,
|
||||||
|
} = this.props;
|
||||||
const { showFieldsStats, fieldStats, fieldCount } = this.state;
|
const { showFieldsStats, fieldStats, fieldCount } = this.state;
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const style = getLogRowStyles(theme);
|
const style = getLogRowStyles(theme);
|
||||||
|
|
||||||
|
const hasDetectedFieldsFunctionality = onClickShowDetectedField && onClickHideDetectedField;
|
||||||
|
const hasFilteringFunctionality = onClickFilterLabel && onClickFilterOutLabel;
|
||||||
|
|
||||||
const toggleFieldButton =
|
const toggleFieldButton =
|
||||||
!isLabel && showDetectedFields && showDetectedFields.includes(parsedKey) ? (
|
!isLabel && showDetectedFields && showDetectedFields.includes(parsedKey) ? (
|
||||||
<IconButton name="eye" className={styles.showingField} title="Hide this field" onClick={this.hideField} />
|
<IconButton name="eye" className={styles.showingField} title="Hide this field" onClick={this.hideField} />
|
||||||
@ -130,7 +146,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
<IconButton name="signal" title={'Ad-hoc statistics'} onClick={this.showStats} />
|
<IconButton name="signal" title={'Ad-hoc statistics'} onClick={this.showStats} />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{isLabel && (
|
{hasFilteringFunctionality && isLabel && (
|
||||||
<>
|
<>
|
||||||
<td className={style.logsDetailsIcon}>
|
<td className={style.logsDetailsIcon}>
|
||||||
<IconButton name="search-plus" title="Filter for value" onClick={this.filterLabel} />
|
<IconButton name="search-plus" title="Filter for value" onClick={this.filterLabel} />
|
||||||
@ -141,12 +157,10 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLabel && (
|
{hasDetectedFieldsFunctionality && !isLabel && (
|
||||||
<>
|
<td className={style.logsDetailsIcon} colSpan={2}>
|
||||||
<td className={style.logsDetailsIcon} colSpan={2}>
|
{toggleFieldButton}
|
||||||
{toggleFieldButton}
|
</td>
|
||||||
</td>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Key - value columns */}
|
{/* Key - value columns */}
|
||||||
|
@ -74,7 +74,7 @@ class UnThemedLogLabelStats extends PureComponent<Props> {
|
|||||||
const otherProportion = otherCount / total;
|
const otherProportion = otherCount / total;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className={style.logsStats}>
|
<td className={style.logsStats} data-testid="logLabelStats">
|
||||||
<div className={style.logsStatsHeader}>
|
<div className={style.logsStatsHeader}>
|
||||||
<div className={style.logsStatsTitle}>
|
<div className={style.logsStatsTitle}>
|
||||||
{label}: {total} of {rowCount} rows have that {isLabel ? 'label' : 'field'}
|
{label}: {total} of {rowCount} rows have that {isLabel ? 'label' : 'field'}
|
||||||
|
@ -41,7 +41,7 @@ interface Props extends Themeable {
|
|||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
allowDetails?: boolean;
|
enableLogDetails: boolean;
|
||||||
logsSortOrder?: LogsSortOrder | null;
|
logsSortOrder?: LogsSortOrder | null;
|
||||||
forceEscape?: boolean;
|
forceEscape?: boolean;
|
||||||
showDetectedFields?: string[];
|
showDetectedFields?: string[];
|
||||||
@ -102,7 +102,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
toggleDetails = () => {
|
toggleDetails = () => {
|
||||||
if (this.props.allowDetails) {
|
if (!this.props.enableLogDetails) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState((state) => {
|
this.setState((state) => {
|
||||||
@ -131,7 +131,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
onClickShowDetectedField,
|
onClickShowDetectedField,
|
||||||
onClickHideDetectedField,
|
onClickHideDetectedField,
|
||||||
highlighterExpressions,
|
highlighterExpressions,
|
||||||
allowDetails,
|
enableLogDetails,
|
||||||
row,
|
row,
|
||||||
showDuplicates,
|
showDuplicates,
|
||||||
showContextToggle,
|
showContextToggle,
|
||||||
@ -171,7 +171,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
{!allowDetails && (
|
{enableLogDetails && (
|
||||||
<td title={showDetails ? 'Hide log details' : 'See log details'} className={style.logsRowToggleDetails}>
|
<td title={showDetails ? 'Hide log details' : 'See log details'} className={style.logsRowToggleDetails}>
|
||||||
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
|
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
|
||||||
</td>
|
</td>
|
||||||
|
@ -17,6 +17,7 @@ describe('LogRows', () => {
|
|||||||
showTime={false}
|
showTime={false}
|
||||||
wrapLogMessage={true}
|
wrapLogMessage={true}
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
|
enableLogDetails={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ describe('LogRows', () => {
|
|||||||
wrapLogMessage={true}
|
wrapLogMessage={true}
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
previewLimit={1}
|
previewLimit={1}
|
||||||
|
enableLogDetails={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -68,6 +70,7 @@ describe('LogRows', () => {
|
|||||||
showTime={false}
|
showTime={false}
|
||||||
wrapLogMessage={true}
|
wrapLogMessage={true}
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
|
enableLogDetails={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -88,6 +91,7 @@ describe('LogRows', () => {
|
|||||||
showTime={false}
|
showTime={false}
|
||||||
wrapLogMessage={true}
|
wrapLogMessage={true}
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
|
enableLogDetails={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -110,6 +114,7 @@ describe('LogRows', () => {
|
|||||||
wrapLogMessage={true}
|
wrapLogMessage={true}
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
logsSortOrder={LogsSortOrder.Ascending}
|
logsSortOrder={LogsSortOrder.Ascending}
|
||||||
|
enableLogDetails={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -133,6 +138,7 @@ describe('LogRows', () => {
|
|||||||
wrapLogMessage={true}
|
wrapLogMessage={true}
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
logsSortOrder={LogsSortOrder.Descending}
|
logsSortOrder={LogsSortOrder.Descending}
|
||||||
|
enableLogDetails={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -21,8 +21,8 @@ export interface Props extends Themeable {
|
|||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
enableLogDetails: boolean;
|
||||||
logsSortOrder?: LogsSortOrder | null;
|
logsSortOrder?: LogsSortOrder | null;
|
||||||
allowDetails?: boolean;
|
|
||||||
previewLimit?: number;
|
previewLimit?: number;
|
||||||
forceEscape?: boolean;
|
forceEscape?: boolean;
|
||||||
showDetectedFields?: string[];
|
showDetectedFields?: string[];
|
||||||
@ -91,7 +91,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
onClickFilterLabel,
|
onClickFilterLabel,
|
||||||
onClickFilterOutLabel,
|
onClickFilterOutLabel,
|
||||||
theme,
|
theme,
|
||||||
allowDetails,
|
enableLogDetails,
|
||||||
previewLimit,
|
previewLimit,
|
||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
logsSortOrder,
|
logsSortOrder,
|
||||||
@ -136,7 +136,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
showDetectedFields={showDetectedFields}
|
showDetectedFields={showDetectedFields}
|
||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
allowDetails={allowDetails}
|
enableLogDetails={enableLogDetails}
|
||||||
onClickFilterLabel={onClickFilterLabel}
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
onClickShowDetectedField={onClickShowDetectedField}
|
onClickShowDetectedField={onClickShowDetectedField}
|
||||||
@ -161,7 +161,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
showDetectedFields={showDetectedFields}
|
showDetectedFields={showDetectedFields}
|
||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
allowDetails={allowDetails}
|
enableLogDetails={enableLogDetails}
|
||||||
onClickFilterLabel={onClickFilterLabel}
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
onClickShowDetectedField={onClickShowDetectedField}
|
onClickShowDetectedField={onClickShowDetectedField}
|
||||||
|
@ -341,6 +341,7 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
showContextToggle={showContextToggle}
|
showContextToggle={showContextToggle}
|
||||||
showLabels={showLabels}
|
showLabels={showLabels}
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
|
enableLogDetails={true}
|
||||||
forceEscape={forceEscape}
|
forceEscape={forceEscape}
|
||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LogRows, CustomScrollbar } from '@grafana/ui';
|
import { LogRows, CustomScrollbar } from '@grafana/ui';
|
||||||
import { PanelProps } from '@grafana/data';
|
import { PanelProps, Field } from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
|
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
|
||||||
|
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||||
|
|
||||||
interface LogsPanelProps extends PanelProps<Options> {}
|
interface LogsPanelProps extends PanelProps<Options> {}
|
||||||
|
|
||||||
export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
||||||
data,
|
data,
|
||||||
timeZone,
|
timeZone,
|
||||||
options: { showLabels, showTime, wrapLogMessage, sortOrder, dedupStrategy },
|
options: { showLabels, showTime, wrapLogMessage, sortOrder, dedupStrategy, enableLogDetails },
|
||||||
}) => {
|
}) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
@ -23,6 +24,10 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
|||||||
const logRows = newResults?.rows || [];
|
const logRows = newResults?.rows || [];
|
||||||
const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
|
const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
|
||||||
|
|
||||||
|
const getFieldLinks = (field: Field, rowIndex: number) => {
|
||||||
|
return getFieldLinksForExplore({ field, rowIndex, range: data.timeRange });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomScrollbar autoHide>
|
<CustomScrollbar autoHide>
|
||||||
<LogRows
|
<LogRows
|
||||||
@ -34,8 +39,9 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
|||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
allowDetails={true}
|
getFieldLinks={getFieldLinks}
|
||||||
logsSortOrder={sortOrder}
|
logsSortOrder={sortOrder}
|
||||||
|
enableLogDetails={enableLogDetails}
|
||||||
/>
|
/>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
);
|
);
|
||||||
|
@ -22,6 +22,12 @@ export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions((build
|
|||||||
description: '',
|
description: '',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
})
|
})
|
||||||
|
.addBooleanSwitch({
|
||||||
|
path: 'enableLogDetails',
|
||||||
|
name: 'Enable log details',
|
||||||
|
description: '',
|
||||||
|
defaultValue: true,
|
||||||
|
})
|
||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'dedupStrategy',
|
path: 'dedupStrategy',
|
||||||
name: 'Deduplication',
|
name: 'Deduplication',
|
||||||
|
@ -4,6 +4,7 @@ export interface Options {
|
|||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
|
enableLogDetails: boolean;
|
||||||
sortOrder: LogsSortOrder;
|
sortOrder: LogsSortOrder;
|
||||||
dedupStrategy: LogsDedupStrategy;
|
dedupStrategy: LogsDedupStrategy;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user