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:
Ivana Huckova 2021-05-18 11:16:29 +02:00 committed by GitHub
parent 10a19ab790
commit a40aef0822
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 104 additions and 43 deletions

View File

@ -1,12 +1,11 @@
import React from 'react';
import { LogDetailsRow, Props } from './LogDetailsRow';
import { GrafanaTheme } from '@grafana/data';
import { mount } from 'enzyme';
import { LogLabelStats } from './LogLabelStats';
import React, { ComponentProps } from 'react';
import { screen, render, fireEvent } from '@testing-library/react';
import { LogDetailsRow } from './LogDetailsRow';
type Props = ComponentProps<typeof LogDetailsRow>;
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
theme: {} as GrafanaTheme,
parsedValue: '',
parsedKey: '',
isLabel: true,
@ -14,40 +13,67 @@ const setup = (propOverrides?: Partial<Props>) => {
getStats: () => null,
onClickFilterLabel: () => {},
onClickFilterOutLabel: () => {},
onClickShowDetectedField: () => {},
onClickHideDetectedField: () => {},
showDetectedFields: [],
};
Object.assign(props, propOverrides);
const wrapper = mount(<LogDetailsRow {...props} />);
return wrapper;
return render(<LogDetailsRow {...props} />);
};
describe('LogDetailsRow', () => {
it('should render parsed key', () => {
const wrapper = setup({ parsedKey: 'test key' });
expect(wrapper.text().includes('test key')).toBe(true);
setup({ parsedKey: 'test key' });
expect(screen.getByText('test key')).toBeInTheDocument();
});
it('should render parsed value', () => {
const wrapper = setup({ parsedValue: 'test value' });
expect(wrapper.text().includes('test value')).toBe(true);
setup({ parsedValue: 'test value' });
expect(screen.getByText('test value')).toBeInTheDocument();
});
it('should render metrics button', () => {
const wrapper = setup();
expect(wrapper.find({ title: 'Ad-hoc statistics' }).hostNodes()).toHaveLength(1);
setup();
expect(screen.getAllByTitle('Ad-hoc statistics')).toHaveLength(1);
});
describe('if props is a label', () => {
it('should render filter label button', () => {
const wrapper = setup();
expect(wrapper.find({ title: 'Filter for value' }).hostNodes()).toHaveLength(1);
setup();
expect(screen.getAllByTitle('Filter for value')).toHaveLength(1);
});
it('should render filter out label button', () => {
const wrapper = setup();
expect(wrapper.find({ title: 'Filter out value' }).hostNodes()).toHaveLength(1);
setup();
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', () => {
const wrapper = setup({
setup({
parsedKey: 'key',
parsedValue: 'value',
getStats: () => {
@ -66,9 +92,10 @@ describe('LogDetailsRow', () => {
},
});
expect(wrapper.find(LogLabelStats).length).toBe(0);
wrapper.find({ title: 'Ad-hoc statistics' }).hostNodes().simulate('click');
expect(wrapper.find(LogLabelStats).length).toBe(1);
expect(wrapper.find(LogLabelStats).contains('another value')).toBeTruthy();
expect(screen.queryByTestId('logLabelStats')).not.toBeInTheDocument();
const adHocStatsButton = screen.getByTitle('Ad-hoc statistics');
fireEvent.click(adHocStatsButton);
expect(screen.getByTestId('logLabelStats')).toBeInTheDocument();
expect(screen.getByTestId('logLabelStats')).toHaveTextContent('another value');
});
});

View File

@ -112,10 +112,26 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
}
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 styles = getStyles(theme);
const style = getLogRowStyles(theme);
const hasDetectedFieldsFunctionality = onClickShowDetectedField && onClickHideDetectedField;
const hasFilteringFunctionality = onClickFilterLabel && onClickFilterOutLabel;
const toggleFieldButton =
!isLabel && showDetectedFields && showDetectedFields.includes(parsedKey) ? (
<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} />
</td>
{isLabel && (
{hasFilteringFunctionality && isLabel && (
<>
<td className={style.logsDetailsIcon}>
<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}>
{toggleFieldButton}
</td>
</>
)}
{/* Key - value columns */}

View File

@ -74,7 +74,7 @@ class UnThemedLogLabelStats extends PureComponent<Props> {
const otherProportion = otherCount / total;
return (
<td className={style.logsStats}>
<td className={style.logsStats} data-testid="logLabelStats">
<div className={style.logsStatsHeader}>
<div className={style.logsStatsTitle}>
{label}: {total} of {rowCount} rows have that {isLabel ? 'label' : 'field'}

View File

@ -41,7 +41,7 @@ interface Props extends Themeable {
showTime: boolean;
wrapLogMessage: boolean;
timeZone: TimeZone;
allowDetails?: boolean;
enableLogDetails: boolean;
logsSortOrder?: LogsSortOrder | null;
forceEscape?: boolean;
showDetectedFields?: string[];
@ -102,7 +102,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
};
toggleDetails = () => {
if (this.props.allowDetails) {
if (!this.props.enableLogDetails) {
return;
}
this.setState((state) => {
@ -131,7 +131,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
onClickShowDetectedField,
onClickHideDetectedField,
highlighterExpressions,
allowDetails,
enableLogDetails,
row,
showDuplicates,
showContextToggle,
@ -171,7 +171,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
</Tooltip>
)}
</td>
{!allowDetails && (
{enableLogDetails && (
<td title={showDetails ? 'Hide log details' : 'See log details'} className={style.logsRowToggleDetails}>
<Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
</td>

View File

@ -17,6 +17,7 @@ describe('LogRows', () => {
showTime={false}
wrapLogMessage={true}
timeZone={'utc'}
enableLogDetails={true}
/>
);
@ -39,6 +40,7 @@ describe('LogRows', () => {
wrapLogMessage={true}
timeZone={'utc'}
previewLimit={1}
enableLogDetails={true}
/>
);
@ -68,6 +70,7 @@ describe('LogRows', () => {
showTime={false}
wrapLogMessage={true}
timeZone={'utc'}
enableLogDetails={true}
/>
);
@ -88,6 +91,7 @@ describe('LogRows', () => {
showTime={false}
wrapLogMessage={true}
timeZone={'utc'}
enableLogDetails={true}
/>
);
@ -110,6 +114,7 @@ describe('LogRows', () => {
wrapLogMessage={true}
timeZone={'utc'}
logsSortOrder={LogsSortOrder.Ascending}
enableLogDetails={true}
/>
);
@ -133,6 +138,7 @@ describe('LogRows', () => {
wrapLogMessage={true}
timeZone={'utc'}
logsSortOrder={LogsSortOrder.Descending}
enableLogDetails={true}
/>
);

View File

@ -21,8 +21,8 @@ export interface Props extends Themeable {
showTime: boolean;
wrapLogMessage: boolean;
timeZone: TimeZone;
enableLogDetails: boolean;
logsSortOrder?: LogsSortOrder | null;
allowDetails?: boolean;
previewLimit?: number;
forceEscape?: boolean;
showDetectedFields?: string[];
@ -91,7 +91,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
onClickFilterLabel,
onClickFilterOutLabel,
theme,
allowDetails,
enableLogDetails,
previewLimit,
getFieldLinks,
logsSortOrder,
@ -136,7 +136,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
showDetectedFields={showDetectedFields}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
allowDetails={allowDetails}
enableLogDetails={enableLogDetails}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
onClickShowDetectedField={onClickShowDetectedField}
@ -161,7 +161,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
showDetectedFields={showDetectedFields}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
allowDetails={allowDetails}
enableLogDetails={enableLogDetails}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
onClickShowDetectedField={onClickShowDetectedField}

View File

@ -341,6 +341,7 @@ export class UnthemedLogs extends PureComponent<Props, State> {
showContextToggle={showContextToggle}
showLabels={showLabels}
showTime={showTime}
enableLogDetails={true}
forceEscape={forceEscape}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}

View File

@ -1,15 +1,16 @@
import React from 'react';
import { LogRows, CustomScrollbar } from '@grafana/ui';
import { PanelProps } from '@grafana/data';
import { PanelProps, Field } from '@grafana/data';
import { Options } from './types';
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
interface LogsPanelProps extends PanelProps<Options> {}
export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
data,
timeZone,
options: { showLabels, showTime, wrapLogMessage, sortOrder, dedupStrategy },
options: { showLabels, showTime, wrapLogMessage, sortOrder, dedupStrategy, enableLogDetails },
}) => {
if (!data) {
return (
@ -23,6 +24,10 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
const logRows = newResults?.rows || [];
const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
const getFieldLinks = (field: Field, rowIndex: number) => {
return getFieldLinksForExplore({ field, rowIndex, range: data.timeRange });
};
return (
<CustomScrollbar autoHide>
<LogRows
@ -34,8 +39,9 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
showTime={showTime}
wrapLogMessage={wrapLogMessage}
timeZone={timeZone}
allowDetails={true}
getFieldLinks={getFieldLinks}
logsSortOrder={sortOrder}
enableLogDetails={enableLogDetails}
/>
</CustomScrollbar>
);

View File

@ -22,6 +22,12 @@ export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions((build
description: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'enableLogDetails',
name: 'Enable log details',
description: '',
defaultValue: true,
})
.addRadio({
path: 'dedupStrategy',
name: 'Deduplication',

View File

@ -4,6 +4,7 @@ export interface Options {
showLabels: boolean;
showTime: boolean;
wrapLogMessage: boolean;
enableLogDetails: boolean;
sortOrder: LogsSortOrder;
dedupStrategy: LogsDedupStrategy;
}