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 { 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');
|
||||
});
|
||||
});
|
||||
|
@ -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 */}
|
||||
|
@ -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'}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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',
|
||||
|
@ -4,6 +4,7 @@ export interface Options {
|
||||
showLabels: boolean;
|
||||
showTime: boolean;
|
||||
wrapLogMessage: boolean;
|
||||
enableLogDetails: boolean;
|
||||
sortOrder: LogsSortOrder;
|
||||
dedupStrategy: LogsDedupStrategy;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user