[Discover] [Logs Explorer] Use LogLevelBadge in log overview tab and Logs Explorer grid (#188615)

## Summary

This PR updates the log level displays in the log overview doc viewer
tab and Logs Explorer content cell to use the shared `LogLevelBadge`
component also used by the Discover logs data source profile for
consistency across the UI.

Discover:

![discover](https://github.com/user-attachments/assets/de9f66d1-19b3-4431-9a88-00d6eb968625)

Logs Explorer:

![logs_explorer](https://github.com/user-attachments/assets/9158a4e0-8526-4cb5-9533-a35c1128e388)

Resolves #188553.

### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Davis McPhee 2024-07-18 13:49:05 -03:00 committed by GitHub
parent 9beda43d63
commit cf3bef6c89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 215 additions and 158 deletions

View file

@ -47,6 +47,7 @@ export {
getLogLevelCoalescedValue,
getLogLevelCoalescedValueLabel,
LogLevelCoalescedValue,
LogLevelBadge,
} from './src';
export type { LogsContextService } from './src';

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './log_level_badge';

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { render, screen } from '@testing-library/react';
import React from 'react';
import { LogLevelBadge } from './log_level_badge';
const renderBadge = (logLevel: string) => {
render(
<LogLevelBadge
logLevel={logLevel}
fallback={<span data-test-subj="logLevelBadge-unknown">{logLevel}</span>}
/>
);
};
describe('LogLevelBadge', () => {
it('renders badge with color based on provided logLevel', () => {
renderBadge('info');
const badge = screen.getByTestId('logLevelBadge-info');
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent('info');
expect(getComputedStyle(badge).getPropertyValue('--euiBadgeBackgroundColor')).toEqual(
'#90b0d1'
);
});
it('renders without a badge if logLevel is not recognized', () => {
renderBadge('unknown_level');
const badge = screen.getByTestId('logLevelBadge-unknown');
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent('unknown_level');
expect(getComputedStyle(badge).getPropertyValue('--euiBadgeBackgroundColor')).toEqual('');
});
});

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { ReactElement } from 'react';
import { EuiBadge, EuiBadgeProps, mathWithUnits, useEuiTheme } from '@elastic/eui';
import { CSSObject } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { getLogLevelCoalescedValue, getLogLevelColor } from '../utils';
const badgeCss: CSSObject = {
maxWidth: mathWithUnits(euiThemeVars.euiSize, (size) => size * 7.5),
};
export const LogLevelBadge = ({
logLevel,
fallback,
'data-test-subj': dataTestSubj = 'logLevelBadge',
...badgeProps
}: Omit<EuiBadgeProps, 'children' | 'color'> & {
logLevel: {};
fallback?: ReactElement;
}) => {
const { euiTheme } = useEuiTheme();
const coalescedValue = getLogLevelCoalescedValue(logLevel);
const color = coalescedValue ? getLogLevelColor(coalescedValue, euiTheme) : undefined;
const castedBadgeProps = badgeProps as EuiBadgeProps;
if (!color || !coalescedValue) {
return fallback ? (
fallback
) : (
<EuiBadge
{...castedBadgeProps}
color="hollow"
data-test-subj={`${dataTestSubj}-unknown`}
css={badgeCss}
>
{logLevel}
</EuiBadge>
);
}
return (
<EuiBadge
{...castedBadgeProps}
color={color}
data-test-subj={`${dataTestSubj}-${coalescedValue}`}
css={badgeCss}
>
{logLevel}
</EuiBadge>
);
};

View file

@ -7,6 +7,7 @@
*/
export * from './types';
export * from './components';
export * from './utils';
export * from './logs_context_service';

View file

@ -5,7 +5,8 @@
"types": [
"jest",
"node",
"react"
"react",
"@testing-library/jest-dom"
]
},
"include": [
@ -23,6 +24,7 @@
"@kbn/field-formats-plugin",
"@kbn/field-types",
"@kbn/i18n",
"@kbn/core-ui-settings-browser"
"@kbn/core-ui-settings-browser",
"@kbn/ui-theme"
]
}

View file

@ -7,53 +7,31 @@
*/
import React from 'react';
import { useEuiTheme } from '@elastic/eui';
import { LogFlyoutDoc } from '@kbn/discover-utils/src';
import { LogFlyoutDoc, LogLevelBadge } from '@kbn/discover-utils/src';
import * as constants from '../../../../common/data_types/logs/constants';
import { ChipWithPopover } from './popover_chip';
const LEVEL_DICT = {
error: 'danger',
warn: 'warning',
info: 'primary',
debug: 'accent',
} as const;
import { ChipPopover } from './popover_chip';
interface LogLevelProps {
level: LogFlyoutDoc['log.level'];
dataTestSubj?: string;
renderInFlyout?: boolean;
}
export function LogLevel({ level, dataTestSubj, renderInFlyout = false }: LogLevelProps) {
const { euiTheme } = useEuiTheme();
export function LogLevel({ level }: LogLevelProps) {
if (!level) return null;
const levelColor = LEVEL_DICT[level as keyof typeof LEVEL_DICT]
? euiTheme.colors[LEVEL_DICT[level as keyof typeof LEVEL_DICT]]
: null;
const truncatedLogLevel = level.length > 10 ? level.substring(0, 10) + '...' : level;
if (renderInFlyout) {
return (
<ChipWithPopover
property={constants.LOG_LEVEL_FIELD}
text={truncatedLogLevel}
borderColor={levelColor}
style={{ width: 'none' }}
dataTestSubj={dataTestSubj}
shouldRenderPopover={!renderInFlyout}
/>
);
}
return (
<ChipWithPopover
<ChipPopover
property={constants.LOG_LEVEL_FIELD}
text={level}
rightSideIcon="arrowDown"
borderColor={levelColor}
style={{ width: '80px', marginTop: '-3px' }}
renderChip={({ handleChipClick, handleChipClickAriaLabel, chipCss }) => (
<LogLevelBadge
logLevel={level}
iconType="arrowDown"
iconSide="right"
onClick={handleChipClick}
onClickAriaLabel={handleChipClickAriaLabel}
css={[chipCss, { width: '80px', paddingInline: '4px' }]}
/>
)}
/>
);
}

View file

@ -33,31 +33,21 @@ const renderCell = (logLevelField: string, record: DataTableRecord) => {
};
describe('getLogLevelBadgeCell', () => {
it('renders badge with color based on provided logLevelField', () => {
it('renders badge if log level is recognized', () => {
const record = buildDataTableRecord({ fields: { 'log.level': 'info' } }, dataViewMock);
renderCell('log.level', record);
const badge = screen.getByTestId('logLevelBadgeCell-info');
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent('info');
expect(getComputedStyle(badge).getPropertyValue('--euiBadgeBackgroundColor')).toEqual(
'#90b0d1'
);
expect(screen.getByTestId('logLevelBadgeCell-info')).toBeInTheDocument();
});
it('renders unknown badge if logLevelField is not recognized', () => {
it('renders unknown if log level is not recognized', () => {
const record = buildDataTableRecord({ fields: { 'log.level': 'unknown_level' } }, dataViewMock);
renderCell('log.level', record);
const badge = screen.getByTestId('logLevelBadgeCell-unknown');
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent('unknown_level');
expect(getComputedStyle(badge).getPropertyValue('--euiBadgeBackgroundColor')).toEqual('');
expect(screen.getByTestId('logLevelBadgeCell-unknown')).toBeInTheDocument();
});
it('renders empty if no matching logLevelField is found', () => {
it('renders empty if no matching log level field is found', () => {
const record = buildDataTableRecord({ fields: { 'log.level': 'info' } }, dataViewMock);
renderCell('log_level', record);
const badge = screen.getByTestId('logLevelBadgeCell-empty');
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent('-');
expect(screen.getByTestId('logLevelBadgeCell-empty')).toBeInTheDocument();
});
});

View file

@ -6,37 +6,28 @@
* Side Public License, v 1.
*/
import { EuiBadge, mathWithUnits, useEuiTheme } from '@elastic/eui';
import type { CSSObject } from '@emotion/react';
import { getLogLevelCoalescedValue, getLogLevelColor } from '@kbn/discover-utils';
import { euiThemeVars } from '@kbn/ui-theme';
import { LogLevelBadge } from '@kbn/discover-utils';
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
import React from 'react';
const badgeCss: CSSObject = {
marginTop: '-4px',
maxWidth: mathWithUnits(euiThemeVars.euiSize, (size) => size * 7.5),
};
const dataTestSubj = 'logLevelBadgeCell';
const badgeCss: CSSObject = { marginTop: '-4px' };
export const getLogLevelBadgeCell =
(logLevelField: string) => (props: DataGridCellValueElementProps) => {
const { euiTheme } = useEuiTheme();
const value = props.row.flattened[logLevelField];
if (!value) {
return <span data-test-subj="logLevelBadgeCell-empty">-</span>;
}
const coalescedValue = getLogLevelCoalescedValue(value);
const color = coalescedValue ? getLogLevelColor(coalescedValue, euiTheme) : undefined;
if (!color || !coalescedValue) {
return <span data-test-subj="logLevelBadgeCell-unknown">{value}</span>;
return <span data-test-subj={`${dataTestSubj}-empty`}>-</span>;
}
return (
<EuiBadge color={color} data-test-subj={`logLevelBadgeCell-${coalescedValue}`} css={badgeCss}>
{value}
</EuiBadge>
<LogLevelBadge
logLevel={value}
fallback={<span data-test-subj={`${dataTestSubj}-unknown`}>{value}</span>}
data-test-subj={dataTestSubj}
css={badgeCss}
/>
);
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useCallback, useState } from 'react';
import React, { ReactElement, useCallback, useState } from 'react';
import {
EuiBadge,
type EuiBadgeProps,
@ -18,7 +18,7 @@ import {
EuiText,
EuiButtonIcon,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { css, SerializedStyles } from '@emotion/react';
import { dynamic } from '@kbn/shared-ux-utility';
import { closeCellActionPopoverText, openCellActionPopoverAriaText } from './translations';
import { FilterInButton } from './filter_in_button';
@ -41,9 +41,6 @@ interface ChipWithPopoverProps {
dataTestSubj?: string;
leftSideIcon?: React.ReactNode;
rightSideIcon?: EuiBadgeProps['iconType'];
borderColor?: string | null;
style?: React.CSSProperties;
shouldRenderPopover?: boolean;
}
export function ChipWithPopover({
@ -52,48 +49,71 @@ export function ChipWithPopover({
dataTestSubj = `dataTablePopoverChip_${property}`,
leftSideIcon,
rightSideIcon,
borderColor,
style,
shouldRenderPopover = true,
}: ChipWithPopoverProps) {
return (
<ChipPopover
property={property}
text={text}
renderChip={({ handleChipClick, handleChipClickAriaLabel, chipCss }) => (
<EuiBadge
color="hollow"
iconType={rightSideIcon}
iconSide="right"
data-test-subj={dataTestSubj}
onClick={handleChipClick}
onClickAriaLabel={handleChipClickAriaLabel}
css={chipCss}
>
<EuiFlexGroup gutterSize="xs">
{leftSideIcon && <EuiFlexItem>{leftSideIcon}</EuiFlexItem>}
<EuiFlexItem>{text}</EuiFlexItem>
</EuiFlexGroup>
</EuiBadge>
)}
/>
);
}
interface ChipPopoverProps {
/**
* ECS mapping for the key
*/
property: string;
/**
* Value for the mapping, which will be displayed
*/
text: string;
renderChip: (props: {
handleChipClick: () => void;
handleChipClickAriaLabel: string;
chipCss: SerializedStyles;
}) => ReactElement;
}
export function ChipPopover({ property, text, renderChip }: ChipPopoverProps) {
const xsFontSize = useEuiFontSize('xs').fontSize;
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const handleChipClick = useCallback(() => {
if (!shouldRenderPopover) return;
setIsPopoverOpen(!isPopoverOpen);
}, [isPopoverOpen, shouldRenderPopover]);
}, [isPopoverOpen]);
const closePopover = () => setIsPopoverOpen(false);
const chipContent = (
<EuiBadge
color="hollow"
iconType={rightSideIcon}
iconSide="right"
data-test-subj={dataTestSubj}
onClick={handleChipClick}
onClickAriaLabel={openCellActionPopoverAriaText}
css={css`
${borderColor ? `border: 2px solid ${borderColor};` : ''}
font-size: ${xsFontSize};
display: flex;
justify-content: center;
${shouldRenderPopover && `margin-right: 4px; margin-top: -3px;`}
cursor: pointer;
`}
style={style}
>
<EuiFlexGroup gutterSize="xs">
{leftSideIcon && <EuiFlexItem>{leftSideIcon}</EuiFlexItem>}
<EuiFlexItem>{text}</EuiFlexItem>
</EuiFlexGroup>
</EuiBadge>
);
return (
<EuiPopover
button={chipContent}
button={renderChip({
handleChipClick,
handleChipClickAriaLabel: openCellActionPopoverAriaText,
chipCss: css`
font-size: ${xsFontSize};
display: flex;
justify-content: center;
margin-right: 4px;
margin-top: -3px;
cursor: pointer;
`,
})}
isOpen={isPopoverOpen}
closePopover={closePopover}
anchorPosition="downCenter"

View file

@ -123,7 +123,7 @@ describe('LogsOverview', () => {
});
it('should display a log level badge when available', async () => {
expect(screen.queryByTestId('unifiedDocViewLogsOverviewLogLevel')).toBeInTheDocument();
expect(screen.queryByTestId('unifiedDocViewLogsOverviewLogLevel-info')).toBeInTheDocument();
});
it('should display a message code block when available', async () => {

View file

@ -7,40 +7,19 @@
*/
import React from 'react';
import { EuiBadge, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { CSSObject } from '@emotion/react';
import { LogDocumentOverview } from '@kbn/discover-utils';
import { LogLevelBadge } from '@kbn/discover-utils';
const LEVEL_DICT = {
error: 'danger',
warn: 'warning',
info: 'primary',
debug: 'accent',
} as const;
type Level = keyof typeof LEVEL_DICT;
const dataTestSubj = 'unifiedDocViewLogsOverviewLogLevel';
const badgeCss: CSSObject = { maxWidth: '100px' };
interface LogLevelProps {
level: LogDocumentOverview['log.level'];
}
export function LogLevel({ level }: LogLevelProps) {
const { euiTheme } = useEuiTheme();
if (!level) return null;
const colorName = LEVEL_DICT[level as Level];
const computedColor = colorName ? euiTheme.colors[colorName] : null;
return (
<EuiBadge
color="hollow"
css={css`
max-width: 100px;
${computedColor ? `border: 2px solid ${computedColor};` : ''}
`}
data-test-subj="unifiedDocViewLogsOverviewLogLevel"
>
{level}
</EuiBadge>
);
return <LogLevelBadge logLevel={level} data-test-subj={dataTestSubj} css={badgeCss} />;
}

View file

@ -169,9 +169,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should render a popover with cell actions when a chip on content column is clicked', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-');
await logLevelChip.click();
// Check Filter In button is present
await testSubjects.existOrFail('dataTableCellAction_addToFilterAction_log.level');
@ -185,9 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should render the table filtered where log.level value is info when filter in action is clicked', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-');
await logLevelChip.click();
// Find Filter In button
@ -196,7 +192,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await filterInButton.click();
const rowWithLogLevelInfo = await testSubjects.findAll('dataTablePopoverChip_log.level');
const rowWithLogLevelInfo = await testSubjects.findAll('*logLevelBadge-');
expect(rowWithLogLevelInfo.length).to.be(4);
});
@ -205,9 +201,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should render the table filtered where log.level value is not info when filter out action is clicked', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-');
await logLevelChip.click();
// Find Filter Out button
@ -216,7 +210,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await filterOutButton.click();
await testSubjects.missingOrFail('dataTablePopoverChip_log.level');
await testSubjects.missingOrFail('*logLevelBadge-');
});
});

View file

@ -170,9 +170,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should render a popover with cell actions when a chip on content column is clicked', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-');
await logLevelChip.click();
// Check Filter In button is present
await testSubjects.existOrFail('dataTableCellAction_addToFilterAction_log.level');
@ -186,9 +184,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should render the table filtered where log.level value is info when filter in action is clicked', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-');
await logLevelChip.click();
// Find Filter In button
@ -197,7 +193,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await filterInButton.click();
const rowWithLogLevelInfo = await testSubjects.findAll('dataTablePopoverChip_log.level');
const rowWithLogLevelInfo = await testSubjects.findAll('*logLevelBadge-');
expect(rowWithLogLevelInfo.length).to.be(4);
});
@ -206,9 +202,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should render the table filtered where log.level value is not info when filter out action is clicked', async () => {
await retry.tryForTime(TEST_TIMEOUT, async () => {
const cellElement = await dataGrid.getCellElement(0, 4);
const logLevelChip = await cellElement.findByTestSubject(
'dataTablePopoverChip_log.level'
);
const logLevelChip = await cellElement.findByTestSubject('*logLevelBadge-');
await logLevelChip.click();
// Find Filter Out button
@ -217,7 +211,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await filterOutButton.click();
await testSubjects.missingOrFail('dataTablePopoverChip_log.level');
await testSubjects.missingOrFail('*logLevelBadge-');
});
});