diff --git a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts index 3453bee1d7369..9d5b3194bdbc0 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts @@ -18,6 +18,8 @@ export type LogDocument = Fields & 'data_stream.type': string; 'data_stream.dataset': string; message?: string; + 'error.message'?: string; + 'event.original'?: string; 'event.dataset': string; 'log.level'?: string; 'host.name'?: string; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts index 18c1fc1c52550..f37847cdcaecf 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts @@ -37,7 +37,7 @@ const scenario: Scenario = async (runOptions) => { .interval('1m') .rate(1) .generator((timestamp) => { - return Array(20) + return Array(3) .fill(0) .map(() => { const index = Math.floor(Math.random() * 3); @@ -63,9 +63,133 @@ const scenario: Scenario = async (runOptions) => { }); }); + const logsWithNoLogLevel = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(3) + .fill(0) + .map(() => { + const index = Math.floor(Math.random() * 3); + return log + .create() + .service(SERVICE_NAMES[index]) + .defaults({ + 'trace.id': generateShortId(), + 'error.message': MESSAGE_LOG_LEVELS[index].message, + 'agent.name': 'synth-agent', + 'orchestrator.cluster.name': CLUSTER[index].clusterName, + 'orchestrator.cluster.id': CLUSTER[index].clusterId, + 'orchestrator.resource.id': generateShortId(), + 'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)], + 'cloud.region': CLOUD_REGION[index], + 'cloud.availability_zone': `${CLOUD_REGION[index]}a`, + 'cloud.project.id': generateShortId(), + 'cloud.instance.id': generateShortId(), + 'log.file.path': `/logs/${generateLongId()}/error.txt`, + }) + .timestamp(timestamp); + }); + }); + + const logsWithErrorMessage = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(3) + .fill(0) + .map(() => { + const index = Math.floor(Math.random() * 3); + return log + .create() + .logLevel(MESSAGE_LOG_LEVELS[index].level) + .service(SERVICE_NAMES[index]) + .defaults({ + 'trace.id': generateShortId(), + 'error.message': MESSAGE_LOG_LEVELS[index].message, + 'agent.name': 'synth-agent', + 'orchestrator.cluster.name': CLUSTER[index].clusterName, + 'orchestrator.cluster.id': CLUSTER[index].clusterId, + 'orchestrator.resource.id': generateShortId(), + 'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)], + 'cloud.region': CLOUD_REGION[index], + 'cloud.availability_zone': `${CLOUD_REGION[index]}a`, + 'cloud.project.id': generateShortId(), + 'cloud.instance.id': generateShortId(), + 'log.file.path': `/logs/${generateLongId()}/error.txt`, + }) + .timestamp(timestamp); + }); + }); + + const logsWithEventMessage = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(3) + .fill(0) + .map(() => { + const index = Math.floor(Math.random() * 3); + return log + .create() + .logLevel(MESSAGE_LOG_LEVELS[index].level) + .service(SERVICE_NAMES[index]) + .defaults({ + 'trace.id': generateShortId(), + 'event.original': MESSAGE_LOG_LEVELS[index].message, + 'agent.name': 'synth-agent', + 'orchestrator.cluster.name': CLUSTER[index].clusterName, + 'orchestrator.cluster.id': CLUSTER[index].clusterId, + 'orchestrator.resource.id': generateShortId(), + 'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)], + 'cloud.region': CLOUD_REGION[index], + 'cloud.availability_zone': `${CLOUD_REGION[index]}a`, + 'cloud.project.id': generateShortId(), + 'cloud.instance.id': generateShortId(), + 'log.file.path': `/logs/${generateLongId()}/error.txt`, + }) + .timestamp(timestamp); + }); + }); + + const logsWithNoMessage = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(3) + .fill(0) + .map(() => { + const index = Math.floor(Math.random() * 3); + return log + .create() + .logLevel(MESSAGE_LOG_LEVELS[index].level) + .service(SERVICE_NAMES[index]) + .defaults({ + 'trace.id': generateShortId(), + 'agent.name': 'synth-agent', + 'orchestrator.cluster.name': CLUSTER[index].clusterName, + 'orchestrator.cluster.id': CLUSTER[index].clusterId, + 'orchestrator.resource.id': generateShortId(), + 'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)], + 'cloud.region': CLOUD_REGION[index], + 'cloud.availability_zone': `${CLOUD_REGION[index]}a`, + 'cloud.project.id': generateShortId(), + 'cloud.instance.id': generateShortId(), + 'log.file.path': `/logs/${generateLongId()}/error.txt`, + }) + .timestamp(timestamp); + }); + }); + return withClient( logsEsClient, - logger.perf('generating_logs', () => logs) + logger.perf('generating_logs', () => [ + logs, + logsWithNoLogLevel, + logsWithErrorMessage, + logsWithEventMessage, + logsWithNoMessage, + ]) ); }, }; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 7dae6af0bb0a7..ca6d3b88e9417 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -85,12 +85,12 @@ pageLoadAssetSize: kibanaUsageCollection: 16463 kibanaUtils: 79713 kubernetesSecurity: 77234 - lens: 43000 + lens: 57135 licenseManagement: 41817 licensing: 29004 links: 44490 lists: 22900 - logExplorer: 54342 + logExplorer: 44977 logsShared: 281060 logstash: 53548 management: 46112 diff --git a/packages/kbn-unified-data-table/src/components/data_table.tsx b/packages/kbn-unified-data-table/src/components/data_table.tsx index 5e9168f62946a..0a0ba657d87bb 100644 --- a/packages/kbn-unified-data-table/src/components/data_table.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.tsx @@ -26,7 +26,6 @@ import { EuiDataGridInMemory, EuiDataGridControlColumn, EuiDataGridCustomBodyProps, - EuiDataGridCellValueElementProps, EuiDataGridCustomToolbarProps, EuiDataGridToolBarVisibilityOptions, EuiDataGridToolBarVisibilityDisplaySelectorOptions, @@ -47,10 +46,11 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { ThemeServiceStart } from '@kbn/react-kibana-context-common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; -import type { +import { UnifiedDataTableSettings, ValueToStringConverter, DataTableColumnTypes, + CustomCellRenderer, } from '../types'; import { getDisplayedColumns } from '../utils/columns'; import { convertValueToString } from '../utils/convert_value_to_string'; @@ -324,10 +324,7 @@ export interface UnifiedDataTableProps { /** * An optional settings for a specified fields rendering like links. Applied only for the listed fields rendering. */ - externalCustomRenderers?: Record< - string, - (props: EuiDataGridCellValueElementProps) => React.ReactNode - >; + externalCustomRenderers?: CustomCellRenderer; /** * Name of the UnifiedDataTable consumer component or application */ diff --git a/packages/kbn-unified-data-table/src/components/data_table_cell_value.tsx b/packages/kbn-unified-data-table/src/components/data_table_cell_value.tsx new file mode 100644 index 0000000000000..cbebecbd1248f --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/data_table_cell_value.tsx @@ -0,0 +1,16 @@ +/* + * 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 from 'react'; + +export const DataTablePopoverCellValue = ({ children }: { children: React.ReactNode }) => { + return {children}; +}; + +// eslint-disable-next-line import/no-default-export +export default DataTablePopoverCellValue; diff --git a/packages/kbn-unified-data-table/src/components/source_document.test.tsx b/packages/kbn-unified-data-table/src/components/source_document.test.tsx new file mode 100644 index 0000000000000..1c3f3fe6d89c5 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/source_document.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import React from 'react'; +import SourceDocument from './source_document'; +import type { EsHitRecord } from '@kbn/discover-utils/src/types'; +import { buildDataTableRecord } from '@kbn/discover-utils'; + +const mockServices = { + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => (value ? value : '-') })), + }, +}; + +const rowsSource: EsHitRecord[] = [ + { + _id: '1', + _index: 'test', + _score: 1, + _source: { bytes: 100, extension: '.gz' }, + highlight: { + extension: ['@kibana-highlighted-field.gz@/kibana-highlighted-field'], + }, + }, +]; + +const build = (hit: EsHitRecord) => buildDataTableRecord(hit, dataViewMock); + +describe('Unified data table source document cell rendering', function () { + it('renders a description list for source type documents', () => { + const rows = rowsSource.map(build); + + const component = mountWithIntl( + false} + maxEntries={100} + isPlainRecord={true} + /> + ); + expect(component.html()).toMatchInlineSnapshot( + `"
_index
test
_score
1
"` + ); + }); +}); diff --git a/packages/kbn-unified-data-table/src/components/source_document.tsx b/packages/kbn-unified-data-table/src/components/source_document.tsx new file mode 100644 index 0000000000000..e29f79f57a6d1 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/source_document.tsx @@ -0,0 +1,127 @@ +/* + * 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, { Fragment } from 'react'; +import type { + DataTableRecord, + EsHitRecord, + FormattedHit, + ShouldShowFieldInTableHandler, +} 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 { formatHit } from '@kbn/discover-utils'; +import { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, +} from '@elastic/eui'; +import classnames from 'classnames'; +import { getInnerColumns } from '../utils/columns'; + +const CELL_CLASS = 'unifiedDataTable__cellValue'; + +export function SourceDocument({ + useTopLevelObjectColumns, + row, + columnId, + dataView, + shouldShowFieldHandler, + maxEntries, + isPlainRecord, + fieldFormats, + dataTestSubj = 'discoverCellDescriptionList', +}: { + useTopLevelObjectColumns: boolean; + row: DataTableRecord; + columnId: string; + dataView: DataView; + shouldShowFieldHandler: ShouldShowFieldInTableHandler; + maxEntries: number; + isPlainRecord?: boolean; + fieldFormats: FieldFormatsStart; + dataTestSubj?: string; +}) { + const pairs: FormattedHit = useTopLevelObjectColumns + ? getTopLevelObjectPairs(row.raw, columnId, dataView, shouldShowFieldHandler).slice( + 0, + maxEntries + ) + : formatHit(row, dataView, shouldShowFieldHandler, maxEntries, fieldFormats); + + return ( + + {pairs.map(([fieldDisplayName, value, fieldName]) => { + // temporary solution for text based mode. As there are a lot of unsupported fields we want to + // hide the empty one from the Document view + if (isPlainRecord && fieldName && row.flattened[fieldName] === null) return null; + return ( + + + {fieldDisplayName} + + + + ); + })} + + ); +} +/** + * Helper function to show top level objects + * this is used for legacy stuff like displaying products of our ecommerce dataset + */ +function getTopLevelObjectPairs( + row: EsHitRecord, + columnId: string, + dataView: DataView, + shouldShowFieldHandler: ShouldShowFieldInTableHandler +) { + const innerColumns = getInnerColumns(row.fields as Record, columnId); + // Put the most important fields first + const highlights: Record = (row.highlight as Record) ?? {}; + const highlightPairs: FormattedHit = []; + const sourcePairs: FormattedHit = []; + Object.entries(innerColumns).forEach(([key, values]) => { + const subField = dataView.getFieldByName(key); + const displayKey = dataView.fields.getByName + ? dataView.fields.getByName(key)?.displayName + : undefined; + const formatter = subField + ? dataView.getFormatterForField(subField) + : { convert: (v: unknown, ...rest: unknown[]) => String(v) }; + const formatted = values + .map((val: unknown) => + formatter.convert(val, 'html', { + field: subField, + hit: row, + }) + ) + .join(', '); + const pairs = highlights[key] ? highlightPairs : sourcePairs; + if (displayKey) { + if (shouldShowFieldHandler(displayKey)) { + pairs.push([displayKey, formatted, key]); + } + } else { + pairs.push([key, formatted, key]); + } + }); + return [...highlightPairs, ...sourcePairs]; +} + +// eslint-disable-next-line import/no-default-export +export default SourceDocument; diff --git a/packages/kbn-unified-data-table/src/components/source_popover_content.tsx b/packages/kbn-unified-data-table/src/components/source_popover_content.tsx new file mode 100644 index 0000000000000..bde3cd80145d1 --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/source_popover_content.tsx @@ -0,0 +1,61 @@ +/* + * 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 type { DataTableRecord } from '@kbn/discover-utils/src/types'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import JsonCodeEditor from './json_code_editor/json_code_editor'; +import { defaultMonacoEditorWidth } from '../constants'; +import { getInnerColumns } from '../utils/columns'; + +export const SourcePopoverContent = ({ + closeButton, + columnId, + row, + useTopLevelObjectColumns, + dataTestSubj = 'dataTableExpandCellActionJsonPopover', +}: { + closeButton: JSX.Element; + columnId: string; + row: DataTableRecord; + useTopLevelObjectColumns: boolean; + dataTestSubj?: string; +}) => { + return ( + + + + {closeButton} + + + + + + + ); +}; + +function getJSON(columnId: string, row: DataTableRecord, useTopLevelObjectColumns: boolean) { + const json = useTopLevelObjectColumns + ? getInnerColumns(row.raw.fields as Record, columnId) + : row.raw; + return json as Record; +} + +// eslint-disable-next-line import/no-default-export +export default SourcePopoverContent; diff --git a/packages/kbn-unified-data-table/src/types.ts b/packages/kbn-unified-data-table/src/types.ts index 77eb6243ac35d..64df4f1000eab 100644 --- a/packages/kbn-unified-data-table/src/types.ts +++ b/packages/kbn-unified-data-table/src/types.ts @@ -6,6 +6,12 @@ * Side Public License, v 1. */ +import React from 'react'; +import { EuiDataGridCellValueElementProps } 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'; + /** * User configurable state of data grid, persisted in saved search */ @@ -27,3 +33,15 @@ export type ValueToStringConverter = ( * Custom column types per column name */ export type DataTableColumnTypes = Record; + +export type DataGridCellValueElementProps = EuiDataGridCellValueElementProps & { + row: DataTableRecord; + dataView: DataView; + fieldFormats: FieldFormatsStart; + closePopover: () => void; +}; + +export type CustomCellRenderer = Record< + string, + (props: DataGridCellValueElementProps) => React.ReactNode +>; diff --git a/packages/kbn-unified-data-table/src/utils/columns.ts b/packages/kbn-unified-data-table/src/utils/columns.ts index f2a72f0a8b650..f9ccadb16c991 100644 --- a/packages/kbn-unified-data-table/src/utils/columns.ts +++ b/packages/kbn-unified-data-table/src/utils/columns.ts @@ -26,3 +26,11 @@ export function getDisplayedColumns(stateColumns: string[] = [], dataView: DataV ? stateColumns : SOURCE_ONLY; } + +export function getInnerColumns(fields: Record, columnId: string) { + return Object.fromEntries( + Object.entries(fields).filter(([key]) => { + return key.startsWith(`${columnId}.`); + }) + ); +} diff --git a/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx b/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx index c59acdc815389..9e211bee1cad7 100644 --- a/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx +++ b/packages/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx @@ -18,6 +18,7 @@ import { CodeEditorProps } from '@kbn/code-editor'; import { buildDataTableRecord } from '@kbn/discover-utils'; import type { EsHitRecord } from '@kbn/discover-utils/types'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { SourceDocument } from '../components/source_document'; jest.mock('@kbn/code-editor', () => { const original = jest.requireActual('@kbn/code-editor'); @@ -165,7 +166,7 @@ describe('Unified data table cell rendering', function () { /> ); expect(component.html()).toMatchInlineSnapshot( - `"
100
"` + `"
100
"` ); }); @@ -192,18 +193,20 @@ describe('Unified data table cell rendering', function () { /> ); expect(component.html()).toMatchInlineSnapshot( - `"
100
"` + `"
100
"` ); findTestSubject(component, 'docTableClosePopover').simulate('click'); expect(closePopoverMockFn).toHaveBeenCalledTimes(1); }); it('renders _source column correctly', () => { + const showFieldHandler = (fieldName: string) => ['extension', 'bytes'].includes(fieldName); + const rows = rowsSource.map(build); const DataTableCellValue = getRenderCellValueFn({ dataView: dataViewMock, - rows: rowsSource.map(build), + rows, useNewFieldsApi: false, - shouldShowFieldHandler: (fieldName) => ['extension', 'bytes'].includes(fieldName), + shouldShowFieldHandler: showFieldHandler, closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, @@ -219,66 +222,19 @@ describe('Unified data table cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component).toMatchInlineSnapshot(` - - - extension - - - - bytesDisplayName - - - - _index - - - - _score - - - - `); + + const sourceDocumentComponent = component.find(SourceDocument); + expect(sourceDocumentComponent.exists()).toBeTruthy(); + + expect(sourceDocumentComponent.props()).toEqual({ + columnId: '_source', + dataView: dataViewMock, + fieldFormats: mockServices.fieldFormats, + useTopLevelObjectColumns: false, + maxEntries: 100, + shouldShowFieldHandler: showFieldHandler, + row: rows[0], + }); }); it('renders _source column correctly when isDetails is set to true', () => { @@ -303,66 +259,57 @@ describe('Unified data table cell rendering', function () { /> ); expect(component).toMatchInlineSnapshot(` - - - - - - - - - - - - + } + columnId="_source" + row={ + Object { + "flattened": Object { + "_index": "test", + "_score": 1, + "bytes": 100, + "extension": ".gz", + }, + "id": "test::1::", + "isAnchor": undefined, + "raw": Object { + "_id": "1", + "_index": "test", + "_score": 1, + "_source": Object { + "bytes": 100, + "extension": ".gz", + }, + "highlight": Object { + "extension": Array [ + "@kibana-highlighted-field.gz@/kibana-highlighted-field", + ], + }, + }, + } + } + useTopLevelObjectColumns={false} + /> `); }); it('renders _source column correctly if on text based mode and have nulls', () => { + const rows = rowsSourceWithEmptyValues.map(build); + const showFieldHandler = (fieldName: string) => ['extension', 'bytes'].includes(fieldName); const DataTableCellValue = getRenderCellValueFn({ dataView: dataViewMock, - rows: rowsSourceWithEmptyValues.map(build), + rows, useNewFieldsApi: false, - shouldShowFieldHandler: (fieldName) => ['extension', 'bytes'].includes(fieldName), + shouldShowFieldHandler: showFieldHandler, closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, @@ -379,61 +326,30 @@ describe('Unified data table cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component).toMatchInlineSnapshot(` - - - bytesDisplayName - - - - _index - - - - _score - - - - `); + + const sourceDocumentComponent = component.find(SourceDocument); + expect(sourceDocumentComponent.exists()).toBeTruthy(); + + expect(sourceDocumentComponent.props()).toEqual({ + columnId: '_source', + dataView: dataViewMock, + fieldFormats: mockServices.fieldFormats, + useTopLevelObjectColumns: false, + maxEntries: 100, + shouldShowFieldHandler: showFieldHandler, + row: rows[0], + isPlainRecord: true, + }); }); it('renders fields-based column correctly', () => { + const rows = rowsFields.map(build); + const showFieldHandler = (fieldName: string) => ['extension', 'bytes'].includes(fieldName); const DataTableCellValue = getRenderCellValueFn({ dataView: dataViewMock, - rows: rowsFields.map(build), + rows, useNewFieldsApi: true, - shouldShowFieldHandler: (fieldName) => ['extension', 'bytes'].includes(fieldName), + shouldShowFieldHandler: showFieldHandler, closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, @@ -449,78 +365,29 @@ describe('Unified data table cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component).toMatchInlineSnapshot(` - - - extension - - - - bytesDisplayName - - - - _index - - - - _score - - - - `); + + const sourceDocumentComponent = component.find(SourceDocument); + expect(sourceDocumentComponent.exists()).toBeTruthy(); + + expect(sourceDocumentComponent.props()).toEqual({ + columnId: '_source', + dataView: dataViewMock, + fieldFormats: mockServices.fieldFormats, + useTopLevelObjectColumns: false, + maxEntries: 100, + shouldShowFieldHandler: showFieldHandler, + row: rows[0], + }); }); it('limits amount of rendered items', () => { + const rows = rowsFields.map(build); + const showFieldHandler = (fieldName: string) => ['extension', 'bytes'].includes(fieldName); const DataTableCellValue = getRenderCellValueFn({ dataView: dataViewMock, - rows: rowsFields.map(build), + rows, useNewFieldsApi: true, - shouldShowFieldHandler: (fieldName) => ['extension', 'bytes'].includes(fieldName), + shouldShowFieldHandler: showFieldHandler, closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, // this is the number of rendered items @@ -537,42 +404,19 @@ describe('Unified data table cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component).toMatchInlineSnapshot(` - - - extension - - - - and 3 more fields - - - - `); + + const sourceDocumentComponent = component.find(SourceDocument); + expect(sourceDocumentComponent.exists()).toBeTruthy(); + + expect(sourceDocumentComponent.props()).toEqual({ + columnId: '_source', + dataView: dataViewMock, + fieldFormats: mockServices.fieldFormats, + useTopLevelObjectColumns: false, + maxEntries: 1, + shouldShowFieldHandler: showFieldHandler, + row: rows[0], + }); }); it('renders fields-based column correctly when isDetails is set to true', () => { @@ -580,7 +424,7 @@ describe('Unified data table cell rendering', function () { dataView: dataViewMock, rows: rowsFields.map(build), useNewFieldsApi: true, - shouldShowFieldHandler: (fieldName) => false, + shouldShowFieldHandler: (fieldName: string) => false, closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, @@ -597,72 +441,67 @@ describe('Unified data table cell rendering', function () { /> ); expect(component).toMatchInlineSnapshot(` - - - - - - - - - - - - + } + columnId="_source" + row={ + Object { + "flattened": Object { + "_index": "test", + "_score": 1, + "bytes": Array [ + 100, + ], + "extension": Array [ + ".gz", + ], + }, + "id": "test::1::", + "isAnchor": undefined, + "raw": Object { + "_id": "1", + "_index": "test", + "_score": 1, + "_source": undefined, + "fields": Object { + "bytes": Array [ + 100, + ], + "extension": Array [ + ".gz", + ], + }, + "highlight": Object { + "extension": Array [ + "@kibana-highlighted-field.gz@/kibana-highlighted-field", + ], + }, + }, + } + } + useTopLevelObjectColumns={false} + /> `); }); it('collect object fields and renders them like _source', () => { + const showFieldHandler = (fieldName: string) => + ['object.value', 'extension', 'bytes'].includes(fieldName); + const rows = rowsFieldsWithTopLevelObject.map(build); const DataTableCellValue = getRenderCellValueFn({ dataView: dataViewMock, - rows: rowsFieldsWithTopLevelObject.map(build), + rows, useNewFieldsApi: true, - shouldShowFieldHandler: (fieldName) => - ['object.value', 'extension', 'bytes'].includes(fieldName), + shouldShowFieldHandler: showFieldHandler, closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, @@ -678,37 +517,31 @@ describe('Unified data table cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component).toMatchInlineSnapshot(` - - - object.value - - - - `); + + const sourceDocumentComponent = component.find(SourceDocument); + expect(sourceDocumentComponent.exists()).toBeTruthy(); + + expect(sourceDocumentComponent.props()).toEqual({ + columnId: 'object', + dataView: dataViewMock, + fieldFormats: mockServices.fieldFormats, + maxEntries: 100, + shouldShowFieldHandler: showFieldHandler, + useTopLevelObjectColumns: true, + row: rows[0], + }); }); it('collect object fields and renders them like _source with fallback for unmapped', () => { (dataViewMock.getFieldByName as jest.Mock).mockReturnValueOnce(undefined); + const showFieldHandler = (fieldName: string) => + ['extension', 'bytes', 'object.value'].includes(fieldName); + const rows = rowsFieldsWithTopLevelObject.map(build); const DataTableCellValue = getRenderCellValueFn({ dataView: dataViewMock, - rows: rowsFieldsWithTopLevelObject.map(build), + rows, useNewFieldsApi: true, - shouldShowFieldHandler: (fieldName) => - ['extension', 'bytes', 'object.value'].includes(fieldName), + shouldShowFieldHandler: showFieldHandler, closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, @@ -724,27 +557,19 @@ describe('Unified data table cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component).toMatchInlineSnapshot(` - - - object.value - - - - `); + + const sourceDocumentComponent = component.find(SourceDocument); + expect(sourceDocumentComponent.exists()).toBeTruthy(); + + expect(sourceDocumentComponent.props()).toEqual({ + columnId: 'object', + dataView: dataViewMock, + fieldFormats: mockServices.fieldFormats, + maxEntries: 100, + shouldShowFieldHandler: showFieldHandler, + useTopLevelObjectColumns: true, + row: rows[0], + }); }); it('collect object fields and renders them as json in details', () => { @@ -770,48 +595,55 @@ describe('Unified data table cell rendering', function () { /> ); expect(component).toMatchInlineSnapshot(` - - - - - - - - - - + } + columnId="object" + row={ + Object { + "flattened": Object { + "_index": "test", + "_score": 1, + "extension": Array [ + ".gz", + ], + "object.value": Array [ + 100, + ], + }, + "id": "test::1::", + "isAnchor": undefined, + "raw": Object { + "_id": "1", + "_index": "test", + "_score": 1, + "_source": undefined, + "fields": Object { + "extension": Array [ + ".gz", + ], "object.value": Array [ 100, ], - } - } - width={370} - /> - - + }, + "highlight": Object { + "extension": Array [ + "@kibana-highlighted-field.gz@/kibana-highlighted-field", + ], + }, + }, + } + } + useTopLevelObjectColumns={true} + /> `); }); @@ -950,7 +782,7 @@ describe('Unified data table cell rendering', function () { dataView: dataViewMock, rows: rowsFieldsUnmapped.map(build), useNewFieldsApi: true, - shouldShowFieldHandler: (fieldName) => ['unmapped'].includes(fieldName), + shouldShowFieldHandler: (fieldName: string) => ['unmapped'].includes(fieldName), closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, @@ -992,21 +824,23 @@ describe('Unified data table cell rendering', function () { ); expect(componentWithDetails).toMatchInlineSnapshot(` - + + /> + void; fieldFormats: FieldFormatsStart; maxEntries: number; - externalCustomRenderers?: Record< - string, - (props: EuiDataGridCellValueElementProps) => React.ReactNode - >; + externalCustomRenderers?: CustomCellRenderer; isPlainRecord?: boolean; }) => { return ({ @@ -67,6 +57,34 @@ export const getRenderCellValueFn = ({ isExpandable, isExpanded, }: EuiDataGridCellValueElementProps) => { + const row = rows ? rows[rowIndex] : undefined; + const field = dataView.fields.getByName(columnId); + const ctx = useContext(UnifiedDataTableContext); + + useEffect(() => { + if (!externalCustomRenderers) { + if (row?.isAnchor) { + setCellProps({ + className: 'dscDocsGrid__cell--highlight', + }); + } else if (ctx.expanded && row && ctx.expanded.id === row.id) { + setCellProps({ + style: { + backgroundColor: ctx.isDarkMode + ? themeDark.euiColorHighlight + : themeLight.euiColorHighlight, + }, + }); + } else { + setCellProps({ style: undefined }); + } + } + }, [ctx, row, setCellProps]); + + if (typeof row === 'undefined') { + return -; + } + if (!!externalCustomRenderers && !!externalCustomRenderers[columnId]) { return ( <> @@ -78,36 +96,14 @@ export const getRenderCellValueFn = ({ isExpandable, isExpanded, colIndex, + row, + dataView, + fieldFormats, + closePopover, })} ); } - const row = rows ? rows[rowIndex] : undefined; - - const field = dataView.fields.getByName(columnId); - const ctx = useContext(UnifiedDataTableContext); - - useEffect(() => { - if (row?.isAnchor) { - setCellProps({ - className: 'dscDocsGrid__cell--highlight', - }); - } else if (ctx.expanded && row && ctx.expanded.id === row.id) { - setCellProps({ - style: { - backgroundColor: ctx.isDarkMode - ? themeDark.euiColorHighlight - : themeLight.euiColorHighlight, - }, - }); - } else { - setCellProps({ style: undefined }); - } - }, [ctx, row, setCellProps]); - - if (typeof row === 'undefined') { - return -; - } /** * when using the fields api this code is used to show top level objects @@ -133,36 +129,17 @@ export const getRenderCellValueFn = ({ } if (field?.type === '_source' || useTopLevelObjectColumns) { - const pairs: FormattedHit = useTopLevelObjectColumns - ? getTopLevelObjectPairs(row.raw, columnId, dataView, shouldShowFieldHandler).slice( - 0, - maxEntries - ) - : formatHit(row, dataView, shouldShowFieldHandler, maxEntries, fieldFormats); - return ( - - {pairs.map(([fieldDisplayName, value, fieldName]) => { - // temporary solution for text based mode. As there are a lot of unsupported fields we want to - // hide the empty one from the Document view - if (isPlainRecord && fieldName && row.flattened[fieldName] === null) return null; - return ( - - - {fieldDisplayName} - - - - ); - })} - + ); } @@ -179,25 +156,6 @@ export const getRenderCellValueFn = ({ }; }; -/** - * Helper function to show top level objects - * this is used for legacy stuff like displaying products of our ecommerce dataset - */ -function getInnerColumns(fields: Record, columnId: string) { - return Object.fromEntries( - Object.entries(fields).filter(([key]) => { - return key.startsWith(`${columnId}.`); - }) - ); -} - -function getJSON(columnId: string, row: DataTableRecord, useTopLevelObjectColumns: boolean) { - const json = useTopLevelObjectColumns - ? getInnerColumns(row.raw.fields as Record, columnId) - : row.raw; - return json as Record; -} - /** * Helper function for the cell popover */ @@ -232,89 +190,40 @@ function renderPopoverContent({ ); if (useTopLevelObjectColumns || field?.type === '_source') { return ( - - - - {closeButton} - - - - - - + ); } return ( - + - + + + {closeButton} ); } -/** - * Helper function to show top level objects - * this is used for legacy stuff like displaying products of our ecommerce dataset - */ -function getTopLevelObjectPairs( - row: EsHitRecord, - columnId: string, - dataView: DataView, - shouldShowFieldHandler: ShouldShowFieldInTableHandler -) { - const innerColumns = getInnerColumns(row.fields as Record, columnId); - // Put the most important fields first - const highlights: Record = (row.highlight as Record) ?? {}; - const highlightPairs: FormattedHit = []; - const sourcePairs: FormattedHit = []; - Object.entries(innerColumns).forEach(([key, values]) => { - const subField = dataView.getFieldByName(key); - const displayKey = dataView.fields.getByName - ? dataView.fields.getByName(key)?.displayName - : undefined; - const formatter = subField - ? dataView.getFormatterForField(subField) - : { convert: (v: unknown, ...rest: unknown[]) => String(v) }; - const formatted = values - .map((val: unknown) => - formatter.convert(val, 'html', { - field: subField, - hit: row, - }) - ) - .join(', '); - const pairs = highlights[key] ? highlightPairs : sourcePairs; - if (displayKey) { - if (shouldShowFieldHandler(displayKey)) { - pairs.push([displayKey, formatted, key]); - } - } else { - pairs.push([key, formatted, key]); - } - }); - return [...highlightPairs, ...sourcePairs]; -} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 187c2b4719249..ebc6aaf20f9e1 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -23,9 +23,14 @@ import type { EsHitRecord } from '@kbn/discover-utils/types'; import { DiscoverMainProvider } from '../../services/discover_state_provider'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; import { DiscoverAppState } from '../../services/discover_app_state_container'; +import { DiscoverCustomization, DiscoverCustomizationProvider } from '../../../../customizations'; +import { createCustomizationService } from '../../../../customizations/customization_service'; +import { DiscoverGrid } from '../../../../components/discover_grid'; setHeaderActionMenuMounter(jest.fn()); +const customisationService = createCustomizationService(); + async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { const services = discoverServiceMock; services.data.query.timefilter.timefilter.getTime = () => { @@ -50,9 +55,11 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { const component = mountWithIntl( - - - + + + + + ); await act(async () => { @@ -100,4 +107,21 @@ describe('Discover documents layout', () => { expect(container.appState.getState().grid?.columns?.someField.width).toEqual(206); }); + + test('should render customisations', async () => { + const customCellRenderer = { + content: () => Test, + }; + const customization: DiscoverCustomization = { + id: 'data_table', + customCellRenderer, + }; + + customisationService.set(customization); + const component = await mountComponent(FetchStatus.COMPLETE, esHitsMock); + const discoverGridComponent = component.find(DiscoverGrid); + expect(discoverGridComponent.exists()).toBeTruthy(); + + expect(discoverGridComponent.prop('externalCustomRenderers')).toEqual(customCellRenderer); + }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index f655af89c0c4c..e3a65fdf02b57 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -65,6 +65,7 @@ import { getRenderCustomToolbarWithElements } from '../../../../components/disco import { useSavedSearchInitial } from '../../services/discover_state_provider'; import { useFetchMoreRecords } from './use_fetch_more_records'; import { SelectedVSAvailableCallout } from './selected_vs_available_callout'; +import { useDiscoverCustomization } from '../../../../customizations'; const containerStyles = css` position: relative; @@ -254,6 +255,8 @@ function DiscoverDocumentsComponent({ [dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc] ); + const externalCustomRenderers = useDiscoverCustomization('data_table')?.customCellRenderer; + const documents = useObservable(stateContainer.dataState.data$.documents$); const callouts = useMemo( @@ -419,6 +422,7 @@ function DiscoverDocumentsComponent({ totalHits={totalHits} onFetchMoreRecords={onFetchMoreRecords} componentsTourSteps={TOUR_STEPS} + externalCustomRenderers={externalCustomRenderers} /> diff --git a/src/plugins/discover/public/customizations/customization_service.ts b/src/plugins/discover/public/customizations/customization_service.ts index 15175c8bad1ae..de3108b9ab53f 100644 --- a/src/plugins/discover/public/customizations/customization_service.ts +++ b/src/plugins/discover/public/customizations/customization_service.ts @@ -7,7 +7,8 @@ */ import { filter, map, Observable, startWith, Subject } from 'rxjs'; -import type { +import { + DataTableCustomization, FlyoutCustomization, SearchBarCustomization, TopNavCustomization, @@ -18,7 +19,8 @@ export type DiscoverCustomization = | FlyoutCustomization | SearchBarCustomization | TopNavCustomization - | UnifiedHistogramCustomization; + | UnifiedHistogramCustomization + | DataTableCustomization; export type DiscoverCustomizationId = DiscoverCustomization['id']; diff --git a/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts b/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts new file mode 100644 index 0000000000000..0fdbebee2ac60 --- /dev/null +++ b/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts @@ -0,0 +1,14 @@ +/* + * 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 type { CustomCellRenderer } from '@kbn/unified-data-table'; + +export interface DataTableCustomization { + id: 'data_table'; + customCellRenderer?: CustomCellRenderer; +} diff --git a/src/plugins/discover/public/customizations/customization_types/index.ts b/src/plugins/discover/public/customizations/customization_types/index.ts index effb7fccf207c..a0e9a1cdb098f 100644 --- a/src/plugins/discover/public/customizations/customization_types/index.ts +++ b/src/plugins/discover/public/customizations/customization_types/index.ts @@ -10,3 +10,4 @@ export * from './flyout_customization'; export * from './search_bar_customization'; export * from './top_nav_customization'; export * from './histogram_customization'; +export * from './data_table_customisation'; diff --git a/x-pack/plugins/log_explorer/common/constants.ts b/x-pack/plugins/log_explorer/common/constants.ts index a73f304a76a5f..b9e85258b9d9c 100644 --- a/x-pack/plugins/log_explorer/common/constants.ts +++ b/x-pack/plugins/log_explorer/common/constants.ts @@ -12,6 +12,8 @@ export const TIMESTAMP_FIELD = '@timestamp'; export const HOST_NAME_FIELD = 'host.name'; export const LOG_LEVEL_FIELD = 'log.level'; export const MESSAGE_FIELD = 'message'; +export const ERROR_MESSAGE_FIELD = 'error.message'; +export const EVENT_ORIGINAL_FIELD = 'event.original'; export const SERVICE_NAME_FIELD = 'service.name'; export const TRACE_ID_FIELD = 'trace.id'; @@ -27,6 +29,9 @@ export const LOG_FILE_PATH_FIELD = 'log.file.path'; export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace'; export const DATASTREAM_DATASET_FIELD = 'data_stream.dataset'; +// Virtual column fields +export const CONTENT_FIELD = 'content'; + // Sizing export const DATA_GRID_COLUMN_WIDTH_SMALL = 240; export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320; @@ -42,7 +47,7 @@ export const DEFAULT_COLUMNS = [ width: DATA_GRID_COLUMN_WIDTH_MEDIUM, }, { - field: MESSAGE_FIELD, + field: CONTENT_FIELD, }, ]; export const DEFAULT_ROWS_PER_PAGE = 100; diff --git a/x-pack/plugins/log_explorer/public/components/common/copy_button.tsx b/x-pack/plugins/log_explorer/public/components/common/copy_button.tsx new file mode 100644 index 0000000000000..fe02a7a872720 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/common/copy_button.tsx @@ -0,0 +1,28 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonEmpty, EuiFlexItem, copyToClipboard } from '@elastic/eui'; +import React from 'react'; +import { copyValueAriaText, copyValueText } from './translations'; + +export const CopyButton = ({ property, value }: { property: string; value: string }) => { + const ariaCopyValueText = copyValueAriaText(property); + + return ( + + copyToClipboard(value)} + data-test-subj={`dataTableCellAction_copyToClipboardAction_${property}`} + > + {copyValueText} + + + ); +}; diff --git a/x-pack/plugins/log_explorer/public/components/common/filter_in_button.tsx b/x-pack/plugins/log_explorer/public/components/common/filter_in_button.tsx new file mode 100644 index 0000000000000..e2f43d1b0c5fc --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/common/filter_in_button.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { generateFilters } from '@kbn/data-plugin/public'; +import { filterForText, actionFilterForText } from './translations'; +import { useVirtualColumnServiceContext } from '../../hooks/use_virtual_column_services'; + +export const FilterInButton = ({ property, value }: { property: string; value: string }) => { + const ariaFilterForText = actionFilterForText(value); + const serviceContext = useVirtualColumnServiceContext(); + const filterManager = serviceContext?.data.query.filterManager; + const dataView = serviceContext.dataView; + + const onFilterForAction = () => { + if (filterManager != null) { + const filter = generateFilters(filterManager, property, [value], '+', dataView); + filterManager.addFilters(filter); + } + }; + + return ( + + + {filterForText} + + + ); +}; diff --git a/x-pack/plugins/log_explorer/public/components/common/filter_out_button.tsx b/x-pack/plugins/log_explorer/public/components/common/filter_out_button.tsx new file mode 100644 index 0000000000000..9291e17cc44fd --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/common/filter_out_button.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { generateFilters } from '@kbn/data-plugin/public'; +import { filterOutText, actionFilterOutText } from './translations'; +import { useVirtualColumnServiceContext } from '../../hooks/use_virtual_column_services'; + +export const FilterOutButton = ({ property, value }: { property: string; value: string }) => { + const ariaFilterOutText = actionFilterOutText(value); + const serviceContext = useVirtualColumnServiceContext(); + const filterManager = serviceContext?.data.query.filterManager; + const dataView = serviceContext.dataView; + + const onFilterOutAction = () => { + if (filterManager != null) { + const filter = generateFilters(filterManager, property, [value], '-', dataView); + filterManager.addFilters(filter); + } + }; + + return ( + + + {filterOutText} + + + ); +}; diff --git a/x-pack/plugins/log_explorer/public/components/common/log_level.tsx b/x-pack/plugins/log_explorer/public/components/common/log_level.tsx new file mode 100644 index 0000000000000..3f2b2ed1a71a4 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/common/log_level.tsx @@ -0,0 +1,56 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { FlyoutDoc } from '../flyout_detail/types'; +import { ChipWithPopover } from './popover_chip'; +import * as constants from '../../../common/constants'; + +const LEVEL_DICT = { + error: 'danger', + warn: 'warning', + info: 'primary', + debug: 'accent', +} as const; + +interface LogLevelProps { + level: FlyoutDoc['log.level']; + dataTestSubj?: string; + renderInFlyout?: boolean; +} + +export function LogLevel({ level, dataTestSubj, renderInFlyout = false }: LogLevelProps) { + const { euiTheme } = useEuiTheme(); + 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; + + if (renderInFlyout) { + return ( + + ); + } + + return ( + + ); +} diff --git a/x-pack/plugins/log_explorer/public/components/common/popover_chip.tsx b/x-pack/plugins/log_explorer/public/components/common/popover_chip.tsx new file mode 100644 index 0000000000000..e56ca010b6a6b --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/common/popover_chip.tsx @@ -0,0 +1,143 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState } from 'react'; +import { + EuiBadge, + type EuiBadgeProps, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + useEuiFontSize, + EuiPopoverFooter, + EuiText, + EuiButtonIcon, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { closeCellActionPopoverText, openCellActionPopoverAriaText } from './translations'; +import { FilterInButton } from './filter_in_button'; +import { FilterOutButton } from './filter_out_button'; +import { CopyButton } from './copy_button'; +import { dynamic } from '../../utils/dynamic'; +const DataTablePopoverCellValue = dynamic( + () => import('@kbn/unified-data-table/src/components/data_table_cell_value') +); + +interface ChipWithPopoverProps { + /** + * ECS mapping for the key + */ + property: string; + /** + * Value for the mapping, which will be displayed + */ + text: string; + dataTestSubj?: string; + leftSideIcon?: EuiBadgeProps['iconType']; + rightSideIcon?: EuiBadgeProps['iconType']; + borderColor?: string | null; + style?: React.CSSProperties; + shouldRenderPopover?: boolean; +} + +export function ChipWithPopover({ + property, + text, + dataTestSubj = `dataTablePopoverChip_${property}`, + leftSideIcon, + rightSideIcon, + borderColor, + style, + shouldRenderPopover = true, +}: ChipWithPopoverProps) { + const xsFontSize = useEuiFontSize('xs').fontSize; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const handleChipClick = useCallback(() => { + if (!shouldRenderPopover) return; + setIsPopoverOpen(!isPopoverOpen); + }, [isPopoverOpen, shouldRenderPopover]); + + const closePopover = () => setIsPopoverOpen(false); + + const chipContent = ( + + + {leftSideIcon && ( + + + + )} + {text} + + + ); + + return ( + + + +
+ + + {property} {text} + + +
+
+ + + +
+ + + + + + + + + + + +
+ ); +} diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/translations.ts b/x-pack/plugins/log_explorer/public/components/common/translations.ts similarity index 78% rename from x-pack/plugins/log_explorer/public/components/flyout_detail/translations.ts rename to x-pack/plugins/log_explorer/public/components/common/translations.ts index e7a5f154a7bef..e0f92a9a14b82 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/translations.ts +++ b/x-pack/plugins/log_explorer/public/components/common/translations.ts @@ -7,8 +7,8 @@ import { i18n } from '@kbn/i18n'; -export const flyoutMessageLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.message', { - defaultMessage: 'Message', +export const flyoutContentLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.message', { + defaultMessage: 'Content breakdown', }); export const flyoutServiceLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.service', { @@ -122,22 +122,30 @@ export const flyoutShipperLabel = i18n.translate('xpack.logExplorer.flyoutDetail defaultMessage: 'Shipper', }); -export const flyoutHoverActionFilterForText = (text: unknown) => +export const actionFilterForText = (text: string) => i18n.translate('xpack.logExplorer.flyoutDetail.value.hover.filterFor', { defaultMessage: 'Filter for this {value}', values: { - value: text as string, + value: text, }, }); -export const flyoutHoverActionFilterOutText = (text: unknown) => +export const actionFilterOutText = (text: string) => i18n.translate('xpack.logExplorer.flyoutDetail.value.hover.filterOut', { defaultMessage: 'Filter out this {value}', values: { - value: text as string, + value: text, }, }); +export const filterOutText = i18n.translate('xpack.logExplorer.popoverAction.filterOut', { + defaultMessage: 'Filter out', +}); + +export const filterForText = i18n.translate('xpack.logExplorer.popoverAction.filterFor', { + defaultMessage: 'Filter for', +}); + export const flyoutHoverActionFilterForFieldPresentText = i18n.translate( 'xpack.logExplorer.flyoutDetail.value.hover.filterForFieldPresent', { @@ -159,6 +167,18 @@ export const flyoutHoverActionCopyToClipboardText = i18n.translate( } ); +export const copyValueText = i18n.translate('xpack.logExplorer.popoverAction.copyValue', { + defaultMessage: 'Copy value', +}); + +export const copyValueAriaText = (fieldName: string) => + i18n.translate('xpack.logExplorer.popoverAction.copyValueAriaText', { + defaultMessage: 'Copy value of {fieldName}', + values: { + fieldName, + }, + }); + export const flyoutAccordionShowMoreText = (count: number) => i18n.translate('xpack.logExplorer.flyoutDetail.section.showMore', { defaultMessage: '+ {hiddenCount} more', @@ -166,3 +186,17 @@ export const flyoutAccordionShowMoreText = (count: number) => hiddenCount: count, }, }); + +export const openCellActionPopoverAriaText = i18n.translate( + 'xpack.logExplorer.popoverAction.openPopover', + { + defaultMessage: 'Open popover', + } +); + +export const closeCellActionPopoverText = i18n.translate( + 'xpack.logExplorer.popoverAction.closePopover', + { + defaultMessage: 'Close popover', + } +); diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx index 07e6b3cc6629a..a16a9b638c1ad 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { LogExplorerFlyoutContentProps } from './types'; -import { useDocDetail } from './use_doc_detail'; +import { useDocDetail } from '../../hooks/use_doc_detail'; import { FlyoutHeader } from './flyout_header'; import { FlyoutHighlights } from './flyout_highlights'; import { DiscoverActionsProvider } from '../../hooks/use_discover_action'; diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_header.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_header.tsx index f49e3d9003949..3d099452b2c94 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_header.tsx +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_header.tsx @@ -6,30 +6,57 @@ */ import React from 'react'; -import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiToken, + EuiText, + EuiAccordion, + useGeneratedHtmlId, + EuiTitle, +} from '@elastic/eui'; import { FlyoutDoc } from './types'; -import { getDocDetailHeaderRenderFlags } from './use_doc_detail'; -import { LogLevel } from './sub_components/log_level'; +import { getMessageWithFallbacks } from '../../hooks/use_doc_detail'; +import { LogLevel } from '../common/log_level'; import { Timestamp } from './sub_components/timestamp'; import * as constants from '../../../common/constants'; -import { flyoutMessageLabel } from './translations'; +import { flyoutContentLabel } from '../common/translations'; import { HoverActionPopover } from './sub_components/hover_popover_action'; export function FlyoutHeader({ doc }: { doc: FlyoutDoc }) { - const { hasTimestamp, hasLogLevel, hasMessage, hasBadges, hasFlyoutHeader } = - getDocDetailHeaderRenderFlags(doc); + const hasTimestamp = Boolean(doc[constants.TIMESTAMP_FIELD]); + const hasLogLevel = Boolean(doc[constants.LOG_LEVEL_FIELD]); + const hasBadges = hasTimestamp || hasLogLevel; + const { field, value } = getMessageWithFallbacks(doc); + const hasMessageField = field && value; + const hasFlyoutHeader = hasMessageField || hasBadges; + + const accordionId = useGeneratedHtmlId({ + prefix: flyoutContentLabel, + }); + + const accordionTitle = ( + +

{flyoutContentLabel}

+
+ ); const logLevelAndTimestamp = ( {hasBadges && ( - {hasLogLevel && ( + {doc[constants.LOG_LEVEL_FIELD] && ( - + )} @@ -43,47 +70,64 @@ export function FlyoutHeader({ doc }: { doc: FlyoutDoc }) { ); - return hasFlyoutHeader ? ( - - {hasMessage ? ( - - - - + const contentField = hasMessageField && ( + + + + + + + + + - - - {flyoutMessageLabel} - - + + {field} + - {logLevelAndTimestamp} - - - {doc[constants.MESSAGE_FIELD]} - - + {logLevelAndTimestamp} - ) : ( - logLevelAndTimestamp - )} - + + + + {value} + + + + + + ); + + return hasFlyoutHeader ? ( + + + {hasMessageField ? contentField : logLevelAndTimestamp} + + ) : null; } diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_highlights.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_highlights.tsx index 43b690807e3aa..2841109bb8ea9 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_highlights.tsx +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_highlights.tsx @@ -31,7 +31,7 @@ import { infraAccordionTitle, otherAccordionTitle, serviceAccordionTitle, -} from './translations'; +} from '../common/translations'; import { HighlightSection } from './sub_components/highlight_section'; import { HighlightContainer } from './sub_components/highlight_container'; import { useFlyoutColumnWidth } from '../../hooks/use_flyouot_column_width'; diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/highlight_field.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/highlight_field.tsx index 7e039497a9bd2..21e595d706c9e 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/highlight_field.tsx +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/highlight_field.tsx @@ -7,10 +7,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTextTruncate } from '@elastic/eui'; import React, { ReactNode } from 'react'; -import { ValuesType } from 'utility-types'; import { dynamic } from '../../../utils/dynamic'; import { HoverActionPopover } from './hover_popover_action'; -import { LogDocument } from '../types'; const HighlightFieldDescription = dynamic(() => import('./highlight_field_description')); @@ -19,7 +17,7 @@ interface HighlightFieldProps { formattedValue: string; icon?: ReactNode; label: string | ReactNode; - value: ValuesType; + value?: string; width: number; } @@ -32,7 +30,7 @@ export function HighlightField({ width, ...props }: HighlightFieldProps) { - return formattedValue ? ( + return formattedValue && value ? ( @@ -47,7 +45,7 @@ export function HighlightField({ - + ; + value: string; title?: string; anchorPosition?: PopoverAnchorPosition; + display?: EuiPopoverProps['display']; } export const HoverActionPopover = ({ @@ -32,6 +32,7 @@ export const HoverActionPopover = ({ field, value, anchorPosition = 'upCenter', + display = 'inline-block', }: HoverPopoverActionProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const leaveTimer = useRef(null); @@ -60,6 +61,7 @@ export const HoverActionPopover = ({ anchorPosition={anchorPosition} panelPaddingSize="s" panelStyle={{ minWidth: '24px' }} + display={display} > {title && ( diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/log_level.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/log_level.tsx deleted file mode 100644 index 88bc8bfe3aff6..0000000000000 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/sub_components/log_level.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiBadge, type EuiBadgeProps } from '@elastic/eui'; -import { FlyoutDoc } from '../types'; - -const LEVEL_DICT: Record = { - error: 'danger', - warn: 'warning', - info: 'primary', - default: 'default', -}; - -interface LogLevelProps { - level: FlyoutDoc['log.level']; -} - -export function LogLevel({ level }: LogLevelProps) { - if (!level) return null; - const levelColor = LEVEL_DICT[level] ?? LEVEL_DICT.default; - - return ( - - {level} - - ); -} diff --git a/x-pack/plugins/log_explorer/public/components/virtual_columns/content.tsx b/x-pack/plugins/log_explorer/public/components/virtual_columns/content.tsx new file mode 100644 index 0000000000000..3f356f44b3a02 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/components/virtual_columns/content.tsx @@ -0,0 +1,145 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; +import { getShouldShowFieldHandler } from '@kbn/discover-utils'; +import { i18n } from '@kbn/i18n'; +import type { DataTableRecord } from '@kbn/discover-utils/src/types'; +import { useDocDetail, getMessageWithFallbacks } from '../../hooks/use_doc_detail'; +import { LogDocument, LogExplorerDiscoverServices } from '../../controller'; +import { LogLevel } from '../common/log_level'; +import * as constants from '../../../common/constants'; +import { dynamic } from '../../utils/dynamic'; +import { VirtualColumnServiceProvider } from '../../hooks/use_virtual_column_services'; + +const SourceDocument = dynamic( + () => import('@kbn/unified-data-table/src/components/source_document') +); + +const DiscoverSourcePopoverContent = dynamic( + () => import('@kbn/unified-data-table/src/components/source_popover_content') +); + +const LogMessage = ({ field, value }: { field?: string; value: string }) => { + const renderFieldPrefix = field && field !== constants.MESSAGE_FIELD; + return ( + + {renderFieldPrefix && ( + + + {field} + + + )} + + + {value} + + + + ); +}; + +const SourcePopoverContent = ({ + row, + columnId, + closePopover, +}: { + row: DataTableRecord; + columnId: string; + closePopover: () => void; +}) => { + const closeButton = ( + + ); + return ( + + ); +}; + +const Content = ({ + row, + dataView, + fieldFormats, + isDetails, + columnId, + closePopover, +}: DataGridCellValueElementProps) => { + const parsedDoc = useDocDetail(row as LogDocument, { dataView }); + const { field, value } = getMessageWithFallbacks(parsedDoc); + const renderLogMessage = field && value; + + const shouldShowFieldHandler = useMemo(() => { + const dataViewFields = dataView.fields.getAll().map((fld) => fld.name); + return getShouldShowFieldHandler(dataViewFields, dataView, true); + }, [dataView]); + + if (isDetails && !renderLogMessage) { + return ; + } + + return ( + + {parsedDoc[constants.LOG_LEVEL_FIELD] && ( + + + + )} + + {renderLogMessage ? ( + + ) : ( + + )} + + + ); +}; + +export const renderContent = + ({ data }: { data: LogExplorerDiscoverServices['data'] }) => + (props: DataGridCellValueElementProps) => { + const { dataView } = props; + const virtualColumnServices = { + data, + dataView, + }; + return ( + + + + ); + }; diff --git a/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts b/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts index 4f10b2e39be44..5430d0aebdd07 100644 --- a/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts +++ b/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts @@ -29,6 +29,8 @@ export interface LogDocument extends DataTableRecord { '@timestamp': string; 'log.level'?: [string]; message?: [string]; + 'error.message'?: string; + 'event.original'?: string; 'host.name'?: string; 'service.name'?: string; @@ -51,6 +53,8 @@ export interface FlyoutDoc { '@timestamp': string; 'log.level'?: string; message?: string; + 'error.message'?: string; + 'event.original'?: string; 'host.name'?: string; 'service.name'?: string; diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_cell_renderer.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_cell_renderer.tsx new file mode 100644 index 0000000000000..8c33a55221d81 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/customizations/custom_cell_renderer.tsx @@ -0,0 +1,16 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { CONTENT_FIELD } from '../../common/constants'; +import { renderContent } from '../components/virtual_columns/content'; + +export const createCustomCellRenderer = ({ data }: { data: DataPublicPluginStart }) => { + return { + [CONTENT_FIELD]: renderContent({ data }), + }; +}; diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx index a1461f4de5fca..73402f4aba1af 100644 --- a/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx @@ -12,7 +12,7 @@ import { FlyoutDetail } from '../components/flyout_detail/flyout_detail'; import { LogExplorerFlyoutContentProps } from '../components/flyout_detail'; import { LogDocument, useLogExplorerControllerContext } from '../controller'; -export const CustomFlyoutContent = ({ +const CustomFlyoutContent = ({ filter, onAddColumn, onRemoveColumn, diff --git a/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx index 60f21be11f948..113f470a988dc 100644 --- a/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx @@ -18,6 +18,7 @@ import type { LogExplorerStartDeps } from '../types'; import { dynamic } from '../utils/dynamic'; import { useKibanaContextForPluginProvider } from '../utils/use_kibana'; import { createCustomSearchBar } from './custom_search_bar'; +import { createCustomCellRenderer } from './custom_cell_renderer'; const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters')); const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector')); @@ -81,6 +82,11 @@ export const createLogExplorerProfileCustomizations = }), }); + customizations.set({ + id: 'data_table', + customCellRenderer: createCustomCellRenderer({ data }), + }); + /** * Hide New, Open and Save settings to prevent working with saved views. */ diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts b/x-pack/plugins/log_explorer/public/hooks/use_doc_detail.ts similarity index 73% rename from x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts rename to x-pack/plugins/log_explorer/public/hooks/use_doc_detail.ts index 2a6baca186ae3..64bb8ffd1f8dd 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts +++ b/x-pack/plugins/log_explorer/public/hooks/use_doc_detail.ts @@ -5,9 +5,13 @@ * 2.0. */ import { formatFieldValue } from '@kbn/discover-utils'; -import * as constants from '../../../common/constants'; -import { useKibanaContextForPlugin } from '../../utils/use_kibana'; -import { FlyoutDoc, LogExplorerFlyoutContentProps, LogDocument } from './types'; +import * as constants from '../../common/constants'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; +import { + FlyoutDoc, + LogExplorerFlyoutContentProps, + LogDocument, +} from '../components/flyout_detail/types'; export function useDocDetail( doc: LogDocument, @@ -33,6 +37,12 @@ export function useDocDetail( const level = levelArray && levelArray.length ? levelArray[0]?.toLowerCase() : undefined; const messageArray = doc.flattened[constants.MESSAGE_FIELD]; const message = messageArray && messageArray.length ? messageArray[0] : undefined; + const errorMessageArray = doc.flattened[constants.ERROR_MESSAGE_FIELD]; + const errorMessage = + errorMessageArray && errorMessageArray.length ? errorMessageArray[0] : undefined; + const eventOriginalArray = doc.flattened[constants.EVENT_ORIGINAL_FIELD]; + const eventOriginal = + eventOriginalArray && eventOriginalArray.length ? eventOriginalArray[0] : undefined; const timestamp = formatField(constants.TIMESTAMP_FIELD); // Service Highlights @@ -61,6 +71,8 @@ export function useDocDetail( [constants.LOG_LEVEL_FIELD]: level, [constants.TIMESTAMP_FIELD]: timestamp, [constants.MESSAGE_FIELD]: message, + [constants.ERROR_MESSAGE_FIELD]: errorMessage, + [constants.EVENT_ORIGINAL_FIELD]: eventOriginal, [constants.SERVICE_NAME_FIELD]: serviceName, [constants.TRACE_ID_FIELD]: traceId, [constants.HOST_NAME_FIELD]: hostname, @@ -78,20 +90,19 @@ export function useDocDetail( }; } -export const getDocDetailHeaderRenderFlags = (doc: FlyoutDoc) => { - const hasTimestamp = Boolean(doc[constants.TIMESTAMP_FIELD]); - const hasLogLevel = Boolean(doc[constants.LOG_LEVEL_FIELD]); - const hasMessage = Boolean(doc[constants.MESSAGE_FIELD]); +export const getMessageWithFallbacks = (doc: FlyoutDoc) => { + const rankingOrder = [ + constants.MESSAGE_FIELD, + constants.ERROR_MESSAGE_FIELD, + constants.EVENT_ORIGINAL_FIELD, + ] as const; - const hasBadges = hasTimestamp || hasLogLevel; + for (const rank of rankingOrder) { + if (doc[rank] !== undefined && doc[rank] !== null) { + return { field: rank, value: doc[rank] }; + } + } - const hasFlyoutHeader = hasBadges || hasMessage; - - return { - hasTimestamp, - hasLogLevel, - hasMessage, - hasBadges, - hasFlyoutHeader, - }; + // If none of the ranks (fallbacks) are present + return { field: undefined }; }; diff --git a/x-pack/plugins/log_explorer/public/hooks/use_hover_actions.tsx b/x-pack/plugins/log_explorer/public/hooks/use_hover_actions.tsx index 71fd5103242c9..d8459215dc366 100644 --- a/x-pack/plugins/log_explorer/public/hooks/use_hover_actions.tsx +++ b/x-pack/plugins/log_explorer/public/hooks/use_hover_actions.tsx @@ -6,21 +6,19 @@ */ import { useMemo, useState } from 'react'; -import { ValuesType } from 'utility-types'; import { copyToClipboard, IconType } from '@elastic/eui'; import { flyoutHoverActionCopyToClipboardText, flyoutHoverActionFilterForFieldPresentText, - flyoutHoverActionFilterForText, - flyoutHoverActionFilterOutText, + actionFilterForText, + actionFilterOutText, flyoutHoverActionToggleColumnText, -} from '../components/flyout_detail/translations'; +} from '../components/common/translations'; import { useDiscoverActionsContext } from './use_discover_action'; -import { LogDocument } from '../components/flyout_detail'; interface HoverActionProps { field: string; - value: ValuesType; + value: string; } export interface HoverActionType { @@ -32,8 +30,8 @@ export interface HoverActionType { } export const useHoverActions = ({ field, value }: HoverActionProps): HoverActionType[] => { - const filterForText = flyoutHoverActionFilterForText(value); - const filterOutText = flyoutHoverActionFilterOutText(value); + const filterForText = actionFilterForText(value); + const filterOutText = actionFilterOutText(value); const actions = useDiscoverActionsContext(); const [columnAdded, setColumnAdded] = useState(false); diff --git a/x-pack/plugins/log_explorer/public/hooks/use_virtual_column_services.tsx b/x-pack/plugins/log_explorer/public/hooks/use_virtual_column_services.tsx new file mode 100644 index 0000000000000..8071b08d80a15 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/hooks/use_virtual_column_services.tsx @@ -0,0 +1,21 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import createContainer from 'constate'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { LogExplorerDiscoverServices } from '../controller'; + +export interface UseVirtualColumnServices { + services: { + data: LogExplorerDiscoverServices['data']; + dataView: DataView; + }; +} + +const useVirtualColumns = ({ services }: UseVirtualColumnServices) => services; + +export const [VirtualColumnServiceProvider, useVirtualColumnServiceContext] = + createContainer(useVirtualColumns); diff --git a/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts b/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts index 5da73c3e5ecb5..60078440ec2a1 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts @@ -5,33 +5,46 @@ * 2.0. */ import expect from '@kbn/expect'; +import moment from 'moment/moment'; +import { log, timerange } from '@kbn/apm-synthtrace-client'; import { FtrProviderContext } from './config'; -const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'message']; +const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'content']; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const retry = getService('retry'); const PageObjects = getPageObjects(['discover', 'observabilityLogExplorer']); + const synthtrace = getService('logSynthtraceEsClient'); + const dataGrid = getService('dataGrid'); + const testSubjects = getService('testSubjects'); + const from = '2023-12-27T10:24:14.035Z'; + const to = '2023-12-27T10:25:14.091Z'; + const TEST_TIMEOUT = 10 * 1000; // 10 secs - describe('Columns selection initialization and update', () => { + const navigateToLogExplorer = () => + PageObjects.observabilityLogExplorer.navigateTo({ + pageState: { + time: { + from, + to, + mode: 'absolute', + }, + }, + }); + + describe('When the log explorer loads', () => { before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' - ); + await synthtrace.index(generateLogsData({ to })); + await navigateToLogExplorer(); }); after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' - ); + await synthtrace.clean(); }); - describe('when the log explorer loads', () => { + describe('columns selection initialization and update', () => { it("should initialize the table columns to logs' default selection", async () => { - await PageObjects.observabilityLogExplorer.navigateTo(); - - await retry.try(async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { expect(await PageObjects.discover.getColumnHeaders()).to.eql(defaultLogColumns); }); }); @@ -39,16 +52,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should restore the table columns from the URL state if exists', async () => { await PageObjects.observabilityLogExplorer.navigateTo({ pageState: { + time: { + from, + to, + mode: 'absolute', + }, columns: [ { field: 'service.name' }, { field: 'host.name' }, - { field: 'message' }, + { field: 'content' }, { field: 'data_stream.namespace' }, ], }, }); - await retry.try(async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { expect(await PageObjects.discover.getColumnHeaders()).to.eql([ ...defaultLogColumns, 'data_stream.namespace', @@ -56,5 +74,235 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); + + describe('render content virtual column properly', async () => { + it('should render log level and log message when present', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(0, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(true); + expect(cellValue.includes('A sample log')).to.be(true); + }); + }); + + it('should render log message when present and skip log level when missing', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(1, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(false); + expect(cellValue.includes('A sample log')).to.be(true); + }); + }); + + it('should render message from error object when top level message not present', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(2, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(true); + expect(cellValue.includes('error.message')).to.be(true); + expect(cellValue.includes('message in error object')).to.be(true); + }); + }); + + it('should render message from event.original when top level message and error.message not present', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(3, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(true); + expect(cellValue.includes('event.original')).to.be(true); + expect(cellValue.includes('message in event original')).to.be(true); + }); + }); + + it('should render the whole JSON when neither message, error.message and event.original are present', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(4, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(true); + + expect(cellValue.includes('error.message')).to.be(false); + expect(cellValue.includes('event.original')).to.be(false); + + const cellAttribute = await cellElement.findByTestSubject( + 'logExplorerCellDescriptionList' + ); + expect(cellAttribute).not.to.be.empty(); + }); + }); + + it('on cell expansion with no message field should open JSON Viewer', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + await dataGrid.clickCellExpandButton(4, 5); + await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover'); + }); + }); + + it('on cell expansion with message field should open regular popover', async () => { + await navigateToLogExplorer(); + await retry.tryForTime(TEST_TIMEOUT, async () => { + await dataGrid.clickCellExpandButton(3, 5); + await testSubjects.existOrFail('euiDataGridExpansionPopover'); + }); + }); + }); + + describe('virtual column cell actions', async () => { + beforeEach(async () => { + await navigateToLogExplorer(); + }); + 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, 5); + const logLevelChip = await cellElement.findByTestSubject( + 'dataTablePopoverChip_log.level' + ); + await logLevelChip.click(); + // Check Filter In button is present + await testSubjects.existOrFail('dataTableCellAction_addToFilterAction_log.level'); + // Check Filter Out button is present + await testSubjects.existOrFail('dataTableCellAction_removeFromFilterAction_log.level'); + // Check Copy button is present + await testSubjects.existOrFail('dataTableCellAction_copyToClipboardAction_log.level'); + }); + }); + + 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, 5); + const logLevelChip = await cellElement.findByTestSubject( + 'dataTablePopoverChip_log.level' + ); + await logLevelChip.click(); + + // Find Filter In button + const filterInButton = await testSubjects.find( + 'dataTableCellAction_addToFilterAction_log.level' + ); + + await filterInButton.click(); + const rowWithLogLevelInfo = await testSubjects.findAll('dataTablePopoverChip_log.level'); + + expect(rowWithLogLevelInfo.length).to.be(4); + }); + }); + + 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, 5); + const logLevelChip = await cellElement.findByTestSubject( + 'dataTablePopoverChip_log.level' + ); + await logLevelChip.click(); + + // Find Filter Out button + const filterOutButton = await testSubjects.find( + 'dataTableCellAction_removeFromFilterAction_log.level' + ); + + await filterOutButton.click(); + await testSubjects.missingOrFail('dataTablePopoverChip_log.level'); + }); + }); + }); }); } + +function generateLogsData({ to, count = 1 }: { to: string; count?: number }) { + const logs = timerange(moment(to).subtract(1, 'second'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().message('A sample log').logLevel('info').timestamp(timestamp); + }) + ); + + const logsWithNoLogLevel = timerange( + moment(to).subtract(2, 'second'), + moment(to).subtract(1, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().message('A sample log').timestamp(timestamp); + }) + ); + + const logsWithErrorMessage = timerange( + moment(to).subtract(3, 'second'), + moment(to).subtract(2, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log + .create() + .logLevel('info') + .timestamp(timestamp) + .defaults({ 'error.message': 'message in error object' }); + }) + ); + + const logsWithEventOriginal = timerange( + moment(to).subtract(4, 'second'), + moment(to).subtract(3, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log + .create() + .logLevel('info') + .timestamp(timestamp) + .defaults({ 'event.original': 'message in event original' }); + }) + ); + + const logsWithNoMessage = timerange( + moment(to).subtract(5, 'second'), + moment(to).subtract(4, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().logLevel('info').timestamp(timestamp); + }) + ); + + const logWithNoMessageNoLogLevel = timerange( + moment(to).subtract(6, 'second'), + moment(to).subtract(5, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().timestamp(timestamp); + }) + ); + + return [ + logs, + logsWithNoLogLevel, + logsWithErrorMessage, + logsWithEventOriginal, + logsWithNoMessage, + logWithNoMessageNoLogLevel, + ]; +} diff --git a/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts b/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts index 583313ec8cb9a..964ebc12b320b 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts @@ -87,15 +87,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(azureDatasetSelectionTitle).to.be('[Azure Logs] activitylogs'); // Go back to previous page selection - await retry.try(async () => { + await retry.tryForTime(30 * 1000, async () => { await browser.goBack(); const backNavigationDatasetSelectionTitle = await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(backNavigationDatasetSelectionTitle).to.be('All logs'); }); - // Go forward to previous page selection - await retry.try(async () => { + await retry.tryForTime(30 * 1000, async () => { await browser.goForward(); const forwardNavigationDatasetSelectionTitle = await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); diff --git a/x-pack/test/functional/apps/observability_log_explorer/header_menu.ts b/x-pack/test/functional/apps/observability_log_explorer/header_menu.ts index f87edc5fc23a5..44ea416d01596 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/header_menu.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/header_menu.ts @@ -71,7 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { '@timestamp', 'service.name', 'host.name', - 'message', + 'content', ]); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts index d19cb269891d7..f73a350a501eb 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts @@ -5,35 +5,48 @@ * 2.0. */ import expect from '@kbn/expect'; +import { log, timerange } from '@kbn/apm-synthtrace-client'; +import moment from 'moment'; import { FtrProviderContext } from '../../../ftr_provider_context'; -const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'message']; +const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'content']; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const retry = getService('retry'); const PageObjects = getPageObjects(['discover', 'observabilityLogExplorer', 'svlCommonPage']); + const synthtrace = getService('svlLogsSynthtraceClient'); + const dataGrid = getService('dataGrid'); + const testSubjects = getService('testSubjects'); + const from = '2023-12-27T10:24:14.035Z'; + const to = '2023-12-27T10:25:14.091Z'; + const TEST_TIMEOUT = 10 * 1000; // 10 secs - describe('Columns selection initialization and update', () => { + const navigateToLogExplorer = () => + PageObjects.observabilityLogExplorer.navigateTo({ + pageState: { + time: { + from, + to, + mode: 'absolute', + }, + }, + }); + + describe('When the log explorer loads', () => { before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' - ); + await synthtrace.index(generateLogsData({ to })); await PageObjects.svlCommonPage.login(); + await navigateToLogExplorer(); }); after(async () => { + await synthtrace.clean(); await PageObjects.svlCommonPage.forceLogout(); - await esArchiver.unload( - 'x-pack/test/functional/es_archives/observability_log_explorer/data_streams' - ); }); - describe('when the log explorer loads', () => { + describe('columns selection initialization and update', () => { it("should initialize the table columns to logs' default selection", async () => { - await PageObjects.observabilityLogExplorer.navigateTo(); - - await retry.try(async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { expect(await PageObjects.discover.getColumnHeaders()).to.eql(defaultLogColumns); }); }); @@ -41,16 +54,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should restore the table columns from the URL state if exists', async () => { await PageObjects.observabilityLogExplorer.navigateTo({ pageState: { + time: { + from, + to, + mode: 'absolute', + }, columns: [ { field: 'service.name' }, { field: 'host.name' }, - { field: 'message' }, + { field: 'content' }, { field: 'data_stream.namespace' }, ], }, }); - await retry.try(async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { expect(await PageObjects.discover.getColumnHeaders()).to.eql([ ...defaultLogColumns, 'data_stream.namespace', @@ -58,5 +76,235 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); + + describe('render content virtual column properly', async () => { + it('should render log level and log message when present', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(0, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(true); + expect(cellValue.includes('A sample log')).to.be(true); + }); + }); + + it('should render log message when present and skip log level when missing', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(1, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(false); + expect(cellValue.includes('A sample log')).to.be(true); + }); + }); + + it('should render message from error object when top level message not present', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(2, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(true); + expect(cellValue.includes('error.message')).to.be(true); + expect(cellValue.includes('message in error object')).to.be(true); + }); + }); + + it('should render message from event.original when top level message and error.message not present', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(3, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(true); + expect(cellValue.includes('event.original')).to.be(true); + expect(cellValue.includes('message in event original')).to.be(true); + }); + }); + + it('should render the whole JSON when neither message, error.message and event.original are present', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(4, 5); + const cellValue = await cellElement.getVisibleText(); + expect(cellValue.includes('info')).to.be(true); + + expect(cellValue.includes('error.message')).to.be(false); + expect(cellValue.includes('event.original')).to.be(false); + + const cellAttribute = await cellElement.findByTestSubject( + 'logExplorerCellDescriptionList' + ); + expect(cellAttribute).not.to.be.empty(); + }); + }); + + it('on cell expansion with no message field should open JSON Viewer', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + await dataGrid.clickCellExpandButton(4, 5); + await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover'); + }); + }); + + it('on cell expansion with message field should open regular popover', async () => { + await navigateToLogExplorer(); + await retry.tryForTime(TEST_TIMEOUT, async () => { + await dataGrid.clickCellExpandButton(3, 5); + await testSubjects.existOrFail('euiDataGridExpansionPopover'); + }); + }); + }); + + describe('virtual column cell actions', async () => { + beforeEach(async () => { + await navigateToLogExplorer(); + }); + 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, 5); + const logLevelChip = await cellElement.findByTestSubject( + 'dataTablePopoverChip_log.level' + ); + await logLevelChip.click(); + // Check Filter In button is present + await testSubjects.existOrFail('dataTableCellAction_addToFilterAction_log.level'); + // Check Filter Out button is present + await testSubjects.existOrFail('dataTableCellAction_removeFromFilterAction_log.level'); + // Check Copy button is present + await testSubjects.existOrFail('dataTableCellAction_copyToClipboardAction_log.level'); + }); + }); + + 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, 5); + const logLevelChip = await cellElement.findByTestSubject( + 'dataTablePopoverChip_log.level' + ); + await logLevelChip.click(); + + // Find Filter In button + const filterInButton = await testSubjects.find( + 'dataTableCellAction_addToFilterAction_log.level' + ); + + await filterInButton.click(); + const rowWithLogLevelInfo = await testSubjects.findAll('dataTablePopoverChip_log.level'); + + expect(rowWithLogLevelInfo.length).to.be(4); + }); + }); + + 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, 5); + const logLevelChip = await cellElement.findByTestSubject( + 'dataTablePopoverChip_log.level' + ); + await logLevelChip.click(); + + // Find Filter Out button + const filterOutButton = await testSubjects.find( + 'dataTableCellAction_removeFromFilterAction_log.level' + ); + + await filterOutButton.click(); + await testSubjects.missingOrFail('dataTablePopoverChip_log.level'); + }); + }); + }); }); } + +function generateLogsData({ to, count = 1 }: { to: string; count?: number }) { + const logs = timerange(moment(to).subtract(1, 'second'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().message('A sample log').logLevel('info').timestamp(timestamp); + }) + ); + + const logsWithNoLogLevel = timerange( + moment(to).subtract(2, 'second'), + moment(to).subtract(1, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().message('A sample log').timestamp(timestamp); + }) + ); + + const logsWithErrorMessage = timerange( + moment(to).subtract(3, 'second'), + moment(to).subtract(2, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log + .create() + .logLevel('info') + .timestamp(timestamp) + .defaults({ 'error.message': 'message in error object' }); + }) + ); + + const logsWithEventOriginal = timerange( + moment(to).subtract(4, 'second'), + moment(to).subtract(3, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log + .create() + .logLevel('info') + .timestamp(timestamp) + .defaults({ 'event.original': 'message in event original' }); + }) + ); + + const logsWithNoMessage = timerange( + moment(to).subtract(5, 'second'), + moment(to).subtract(4, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().logLevel('info').timestamp(timestamp); + }) + ); + + const logWithNoMessageNoLogLevel = timerange( + moment(to).subtract(6, 'second'), + moment(to).subtract(5, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().timestamp(timestamp); + }) + ); + + return [ + logs, + logsWithNoLogLevel, + logsWithErrorMessage, + logsWithEventOriginal, + logsWithNoMessage, + logWithNoMessageNoLogLevel, + ]; +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts index 0bb8da7a911b9..d245d2aa71911 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts @@ -93,7 +93,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { '@timestamp', 'service.name', 'host.name', - 'message', + 'content', ]); }); await retry.try(async () => { @@ -150,9 +150,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { expect(await PageObjects.discover.getColumnHeaders()).not.to.eql([ '@timestamp', + 'content', 'service.name', 'host.name', - 'message', ]); });