Skip to content

Commit

Permalink
[Discover] Add log level badge cell renderer for logs profile (#188281)
Browse files Browse the repository at this point in the history
## Summary

This PR adds a log level badge cell render for `log.level` and
`log_level` fields when the logs data source profile is active in
Discover:

![log_level_badge](https://github.com/user-attachments/assets/fa90d5d6-538c-4b9e-bad5-912db23ee41c)

Resolves #186567.

### 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)
  • Loading branch information
davismcphee authored Jul 17, 2024
1 parent 5f843a8 commit ab8bfda
Show file tree
Hide file tree
Showing 17 changed files with 417 additions and 45 deletions.
8 changes: 4 additions & 4 deletions packages/kbn-unified-data-table/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
* Side Public License, v 1.
*/

import React from 'react';
import { EuiDataGridCellValueElementProps, type EuiDataGridColumn } from '@elastic/eui';
import type { ReactElement } from 'react';
import type { EuiDataGridCellValueElementProps, EuiDataGridColumn } from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils/src/types';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { EuiDataGridControlColumn } from '@elastic/eui/src/components/datagrid/data_grid_types';
import type { EuiDataGridControlColumn } from '@elastic/eui/src/components/datagrid/data_grid_types';
import type { DatatableColumnMeta } from '@kbn/expressions-plugin/common';

/**
Expand Down Expand Up @@ -57,7 +57,7 @@ export type DataGridCellValueElementProps = EuiDataGridCellValueElementProps & {

export type CustomCellRenderer = Record<
string,
(props: DataGridCellValueElementProps) => React.ReactNode
(props: DataGridCellValueElementProps) => ReactElement
>;

export interface CustomGridColumnProps {
Expand Down
30 changes: 16 additions & 14 deletions packages/kbn-unified-data-table/src/utils/get_render_cell_value.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,24 @@ export const getRenderCellValueFn = ({
return <span className={CELL_CLASS}>-</span>;
}

if (!!externalCustomRenderers && !!externalCustomRenderers[columnId]) {
const CustomCellRenderer = externalCustomRenderers?.[columnId];

if (CustomCellRenderer) {
return (
<span className={CELL_CLASS}>
{externalCustomRenderers[columnId]({
rowIndex,
columnId,
isDetails,
setCellProps,
isExpandable,
isExpanded,
colIndex,
row,
dataView,
fieldFormats,
closePopover,
})}
<CustomCellRenderer
rowIndex={rowIndex}
columnId={columnId}
isDetails={isDetails}
setCellProps={setCellProps}
isExpandable={isExpandable}
isExpanded={isExpanded}
colIndex={colIndex}
row={row}
dataView={dataView}
fieldFormats={fieldFormats}
closePopover={closePopover}
/>
</span>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/discover/common/data_types/logs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,5 @@ export const FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT = [

export const DEFAULT_ALLOWED_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat'];
export const DEFAULT_ALLOWED_LOGS_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat'];

export const LOG_LEVEL_FIELDS = ['log.level', 'log_level'];
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { buildDataTableRecord, DataTableRecord } from '@kbn/discover-utils';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { getLogLevelBadgeCell } from './log_level_badge_cell';

const renderCell = (logLevelField: string, record: DataTableRecord) => {
const LogLevelBadgeCell = getLogLevelBadgeCell(logLevelField);
render(
<LogLevelBadgeCell
rowIndex={0}
colIndex={0}
columnId="log.level"
isExpandable={false}
isExpanded={false}
isDetails={false}
row={record}
dataView={dataViewMock}
fieldFormats={fieldFormatsMock}
setCellProps={() => {}}
closePopover={() => {}}
/>
);
};

describe('getLogLevelBadgeCell', () => {
it('renders badge with color based on provided logLevelField', () => {
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'
);
});

it('renders unknown badge if logLevelField 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('');
});

it('renders empty if no matching logLevelField 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('-');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { 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 type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
import React from 'react';

const badgeCss: CSSObject = {
marginTop: '-4px',
maxWidth: mathWithUnits(euiThemeVars.euiSize, (size) => size * 7.5),
};

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 (
<EuiBadge color={color} data-test-subj={`logLevelBadgeCell-${coalescedValue}`} css={badgeCss}>
{value}
</EuiBadge>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Side Public License, v 1.
*/

import React from 'react';
import { getDataTableRecords } from '../../__fixtures__/real_hits';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
import {
Expand All @@ -30,7 +31,7 @@ export const createContextAwarenessMocks = ({
profile: {
getCellRenderers: jest.fn((prev) => () => ({
...prev(),
rootProfile: () => 'root-profile',
rootProfile: () => <>root-profile</>,
})),
},
resolve: jest.fn(() => ({
Expand All @@ -46,7 +47,7 @@ export const createContextAwarenessMocks = ({
profile: {
getCellRenderers: jest.fn((prev) => () => ({
...prev(),
rootProfile: () => 'data-source-profile',
rootProfile: () => <>data-source-profile</>,
})),
},
resolve: jest.fn(() => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 { LOG_LEVEL_FIELDS } from '../../../../../common/data_types/logs/constants';
import { getLogLevelBadgeCell } from '../../../../components/data_types/logs/log_level_badge_cell';
import type { DataSourceProfileProvider } from '../../../profiles';

export const getCellRenderers: DataSourceProfileProvider['profile']['getCellRenderers'] =
(prev) => () => ({
...prev(),
...LOG_LEVEL_FIELDS.reduce(
(acc, field) => ({
...acc,
[field]: getLogLevelBadgeCell(field),
[`${field}.keyword`]: getLogLevelBadgeCell(`${field}.keyword`),
}),
{}
),
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import {
getLogLevelColor,
} from '@kbn/discover-utils';
import type { UnifiedDataTableProps } from '@kbn/unified-data-table';
import { LOG_LEVEL_FIELDS } from '../../../../../common/data_types/logs/constants';
import type { DataSourceProfileProvider } from '../../../profiles';

const LOG_LEVEL_FIELDS = ['log.level', 'log_level'];

export const getRowIndicatorProvider: DataSourceProfileProvider['profile']['getRowIndicatorProvider'] =

() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*/

export { getRowIndicatorProvider } from './get_row_indicator_provider';
export { getCellRenderers } from './get_cell_renderers';
Original file line number Diff line number Diff line change
Expand Up @@ -78,32 +78,32 @@ describe('logsDataSourceProfileProvider', () => {
).toEqual(RESOLUTION_MISMATCH);
});

describe('getRowIndicator', () => {
const dataViewWithLogLevel = createStubIndexPattern({
spec: {
title: VALID_INDEX_PATTERN,
fields: {
'log.level': {
name: 'log.level',
type: 'string',
esTypes: ['keyword'],
aggregatable: true,
searchable: true,
count: 0,
readFromDocValues: false,
scripted: false,
isMapped: true,
},
const dataViewWithLogLevel = createStubIndexPattern({
spec: {
title: VALID_INDEX_PATTERN,
fields: {
'log.level': {
name: 'log.level',
type: 'string',
esTypes: ['keyword'],
aggregatable: true,
searchable: true,
count: 0,
readFromDocValues: false,
scripted: false,
isMapped: true,
},
},
});
},
});

const dataViewWithoutLogLevel = createStubIndexPattern({
spec: {
title: VALID_INDEX_PATTERN,
},
});
const dataViewWithoutLogLevel = createStubIndexPattern({
spec: {
title: VALID_INDEX_PATTERN,
},
});

describe('getRowIndicator', () => {
it('should return the correct color for a given log level', () => {
const row = buildDataTableRecord({ fields: { 'log.level': 'info' } });
const euiTheme = { euiTheme: { colors: {} } } as unknown as EuiThemeComputed;
Expand Down Expand Up @@ -140,4 +140,17 @@ describe('logsDataSourceProfileProvider', () => {
expect(getRowIndicator).toBeUndefined();
});
});

describe('getCellRenderers', () => {
it('should return cell renderers for log level fields', () => {
const getCellRenderers = logsDataSourceProfileProvider.profile.getCellRenderers?.(() => ({}));
const cellRenderers = getCellRenderers?.();

expect(cellRenderers).toBeDefined();
expect(cellRenderers?.['log.level']).toBeDefined();
expect(cellRenderers?.['log.level.keyword']).toBeDefined();
expect(cellRenderers?.log_level).toBeDefined();
expect(cellRenderers?.['log_level.keyword']).toBeDefined();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import {
} from '../../profiles';
import { ProfileProviderServices } from '../profile_provider_services';
import { getRowIndicatorProvider } from './accessors';
import { getCellRenderers } from './accessors';

export const createLogsDataSourceProfileProvider = (
services: ProfileProviderServices
): DataSourceProfileProvider => ({
profileId: 'logs-data-source-profile',
profile: {
getRowIndicatorProvider,
getCellRenderers,
},
resolve: (params) => {
const indexPattern = extractIndexPatternFrom(params);
Expand Down
Loading

0 comments on commit ab8bfda

Please sign in to comment.