From 3a747006cf54bbe4ca310b3c3869c3b102cce674 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Wed, 11 Sep 2024 13:46:36 +0300 Subject: [PATCH] [OneDiscover][UnifiedDocViewer] Add filtering for selected fields (#191930) - Closes https://github.com/elastic/kibana/issues/191536 ## Summary This PR adds "Selected fields only" toggle to UnifiedDocViewer. The selected state will be persisted under `unifiedDocViewer:showOnlySelectedFields` in Local Storage. Screenshot 2024-09-11 at 10 08 26 ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Davis McPhee Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-discover-utils/index.ts | 2 + .../src/__mocks__/data_view.ts | 6 + packages/kbn-discover-utils/src/types.ts | 12 + .../src/utils/get_visible_columns.test.tsx | 195 ++++++++++++++ .../src/utils/get_visible_columns.ts | 49 ++++ .../kbn-discover-utils/src/utils/index.ts | 1 + packages/kbn-discover-utils/tsconfig.json | 3 +- packages/kbn-discover-utils/types.ts | 1 + .../src/components/data_table.tsx | 8 +- .../components/data_table_columns.test.tsx | 185 +------------ .../src/components/data_table_columns.tsx | 36 --- packages/kbn-unified-data-table/src/types.ts | 13 +- .../src/services/types.ts | 15 +- packages/kbn-unified-doc-viewer/tsconfig.json | 1 - .../esql_datagrid/public/data_grid.tsx | 11 +- .../esql_datagrid/public/row_viewer.tsx | 3 +- .../doc_viewer_flyout/doc_viewer_flyout.tsx | 3 +- .../doc_viewer_table/table.test.tsx | 168 ++++++++++++ .../components/doc_viewer_table/table.tsx | 247 ++++++++++++------ src/plugins/unified_doc_viewer/tsconfig.json | 1 - .../apps/discover/group3/_doc_viewer.ts | 145 ++++++++++ 21 files changed, 759 insertions(+), 346 deletions(-) create mode 100644 packages/kbn-discover-utils/src/utils/get_visible_columns.test.tsx create mode 100644 packages/kbn-discover-utils/src/utils/get_visible_columns.ts create mode 100644 src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.test.tsx diff --git a/packages/kbn-discover-utils/index.ts b/packages/kbn-discover-utils/index.ts index 7119ac31d5baf..fda3fbdd48ad0 100644 --- a/packages/kbn-discover-utils/index.ts +++ b/packages/kbn-discover-utils/index.ts @@ -52,6 +52,8 @@ export { LogLevelCoalescedValue, LogLevelBadge, getFieldValue, + getVisibleColumns, + canPrependTimeFieldColumn, } from './src'; export type { LogsContextService } from './src'; diff --git a/packages/kbn-discover-utils/src/__mocks__/data_view.ts b/packages/kbn-discover-utils/src/__mocks__/data_view.ts index 5034ed9dc008e..daa8835e817c4 100644 --- a/packages/kbn-discover-utils/src/__mocks__/data_view.ts +++ b/packages/kbn-discover-utils/src/__mocks__/data_view.ts @@ -140,3 +140,9 @@ export const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields: shallowMockedFields, }); + +export const dataViewMockWithTimeField = buildDataViewMock({ + name: 'the-data-view', + fields: shallowMockedFields, + timeFieldName: '@timestamp', +}); diff --git a/packages/kbn-discover-utils/src/types.ts b/packages/kbn-discover-utils/src/types.ts index a2afe245308bb..dc720c02dd4ea 100644 --- a/packages/kbn-discover-utils/src/types.ts +++ b/packages/kbn-discover-utils/src/types.ts @@ -8,6 +8,7 @@ */ import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { DatatableColumnMeta } from '@kbn/expressions-plugin/common'; export type { IgnoredReason, ShouldShowFieldInTableHandler } from './utils'; @@ -41,6 +42,17 @@ export interface DataTableRecord { isAnchor?: boolean; } +/** + * Custom column types per column name + */ +export type DataTableColumnsMeta = Record< + string, + { + type: DatatableColumnMeta['type']; + esType?: DatatableColumnMeta['esType']; + } +>; + type FormattedHitPair = readonly [ fieldDisplayName: string, formattedValue: string, diff --git a/packages/kbn-discover-utils/src/utils/get_visible_columns.test.tsx b/packages/kbn-discover-utils/src/utils/get_visible_columns.test.tsx new file mode 100644 index 0000000000000..88877209d0528 --- /dev/null +++ b/packages/kbn-discover-utils/src/utils/get_visible_columns.test.tsx @@ -0,0 +1,195 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DatatableColumnType } from '@kbn/expressions-plugin/common'; +import { + dataViewMock as dataViewMockWithoutTimeField, + dataViewMockWithTimeField, +} from '../__mocks__'; +import { getVisibleColumns, canPrependTimeFieldColumn } from './get_visible_columns'; + +describe('getVisibleColumns utils', function () { + describe('getVisibleColumns', () => { + it('returns grid columns without time column when data view has no timestamp field', () => { + const actual = getVisibleColumns( + ['extension', 'message'], + dataViewMockWithoutTimeField, + true + ) as string[]; + expect(actual).toEqual(['extension', 'message']); + }); + + it('returns grid columns without time column when showTimeCol is falsy', () => { + const actual = getVisibleColumns( + ['extension', 'message'], + dataViewMockWithTimeField, + false + ) as string[]; + expect(actual).toEqual(['extension', 'message']); + }); + + it('returns grid columns with time column when data view has timestamp field', () => { + const actual = getVisibleColumns( + ['extension', 'message'], + dataViewMockWithTimeField, + true + ) as string[]; + expect(actual).toEqual(['@timestamp', 'extension', 'message']); + }); + }); + + describe('canPrependTimeFieldColumn', () => { + function buildColumnTypes(dataView: DataView) { + const columnsMeta: Record< + string, + { type: DatatableColumnType; esType?: string | undefined } + > = {}; + for (const field of dataView.fields) { + columnsMeta[field.name] = { type: field.type as DatatableColumnType }; + } + return columnsMeta; + } + + describe('dataView with timeField', () => { + it('should forward showTimeCol if no _source columns is passed', () => { + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['extension', 'message'], + dataViewMockWithTimeField.timeFieldName, + buildColumnTypes(dataViewMockWithTimeField), + showTimeCol, + false + ) + ).toBe(showTimeCol); + } + }); + + it('should return false if no _source columns is passed, text-based datasource', () => { + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['extension', 'message'], + dataViewMockWithTimeField.timeFieldName, + buildColumnTypes(dataViewMockWithTimeField), + showTimeCol, + true + ) + ).toBe(false); + } + }); + + it('should forward showTimeCol if _source column is passed', () => { + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['_source'], + dataViewMockWithTimeField.timeFieldName, + buildColumnTypes(dataViewMockWithTimeField), + showTimeCol, + false + ) + ).toBe(showTimeCol); + } + }); + + it('should forward showTimeCol if _source column is passed, text-based datasource', () => { + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['_source'], + dataViewMockWithTimeField.timeFieldName, + buildColumnTypes(dataViewMockWithTimeField), + showTimeCol, + true + ) + ).toBe(showTimeCol); + } + }); + + it('should return false if _source column is passed but time field is not returned, text-based datasource', () => { + // ... | DROP @timestamp test case + const columnsMeta = buildColumnTypes(dataViewMockWithTimeField); + if (dataViewMockWithTimeField.timeFieldName) { + delete columnsMeta[dataViewMockWithTimeField.timeFieldName]; + } + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['_source'], + dataViewMockWithTimeField.timeFieldName, + columnsMeta, + showTimeCol, + true + ) + ).toBe(false); + } + }); + }); + + describe('dataView without timeField', () => { + it('should return false if no _source columns is passed', () => { + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['extension', 'message'], + dataViewMockWithoutTimeField.timeFieldName, + buildColumnTypes(dataViewMockWithoutTimeField), + showTimeCol, + false + ) + ).toBe(false); + } + }); + + it('should return false if no _source columns is passed, text-based datasource', () => { + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['extension', 'message'], + dataViewMockWithoutTimeField.timeFieldName, + buildColumnTypes(dataViewMockWithoutTimeField), + showTimeCol, + true + ) + ).toBe(false); + } + }); + + it('should return false if _source column is passed', () => { + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['_source'], + dataViewMockWithoutTimeField.timeFieldName, + buildColumnTypes(dataViewMockWithoutTimeField), + showTimeCol, + false + ) + ).toBe(false); + } + }); + + it('should return false if _source column is passed, text-based datasource', () => { + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['_source'], + dataViewMockWithoutTimeField.timeFieldName, + buildColumnTypes(dataViewMockWithoutTimeField), + showTimeCol, + true + ) + ).toBe(false); + } + }); + }); + }); +}); diff --git a/packages/kbn-discover-utils/src/utils/get_visible_columns.ts b/packages/kbn-discover-utils/src/utils/get_visible_columns.ts new file mode 100644 index 0000000000000..69c4390d61e08 --- /dev/null +++ b/packages/kbn-discover-utils/src/utils/get_visible_columns.ts @@ -0,0 +1,49 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DataTableColumnsMeta } from '../types'; + +export function canPrependTimeFieldColumn( + columns: string[] | undefined, + timeFieldName: string | undefined, + columnsMeta: DataTableColumnsMeta | undefined, + showTimeCol: boolean, // based on Advanced Settings `doc_table:hideTimeColumn` + isESQLMode: boolean +) { + if (!showTimeCol || !timeFieldName) { + return false; + } + + if (isESQLMode) { + return ( + !!columns && !!columnsMeta && timeFieldName in columnsMeta && columns.includes('_source') + ); + } + + return true; +} + +export function getVisibleColumns( + columns: string[], + dataView: DataView, + shouldPrependTimeFieldColumn: boolean +) { + const timeFieldName = dataView.timeFieldName; + + if ( + shouldPrependTimeFieldColumn && + timeFieldName && + !columns.find((col) => col === timeFieldName) + ) { + return [timeFieldName, ...columns]; + } + + return columns; +} diff --git a/packages/kbn-discover-utils/src/utils/index.ts b/packages/kbn-discover-utils/src/utils/index.ts index 4066ce34bdb01..e408d7eb1c163 100644 --- a/packages/kbn-discover-utils/src/utils/index.ts +++ b/packages/kbn-discover-utils/src/utils/index.ts @@ -18,4 +18,5 @@ export * from './get_should_show_field_handler'; export * from './nested_fields'; export * from './get_field_value'; export * from './calc_field_counts'; +export * from './get_visible_columns'; export { isLegacyTableEnabled } from './is_legacy_table_enabled'; diff --git a/packages/kbn-discover-utils/tsconfig.json b/packages/kbn-discover-utils/tsconfig.json index de0b747eda555..5a5fe4c69636e 100644 --- a/packages/kbn-discover-utils/tsconfig.json +++ b/packages/kbn-discover-utils/tsconfig.json @@ -25,6 +25,7 @@ "@kbn/field-types", "@kbn/i18n", "@kbn/core-ui-settings-browser", - "@kbn/ui-theme" + "@kbn/ui-theme", + "@kbn/expressions-plugin" ] } diff --git a/packages/kbn-discover-utils/types.ts b/packages/kbn-discover-utils/types.ts index 436d9617614ff..dfbb54f1f09ca 100644 --- a/packages/kbn-discover-utils/types.ts +++ b/packages/kbn-discover-utils/types.ts @@ -9,6 +9,7 @@ export type { DataTableRecord, + DataTableColumnsMeta, EsHitRecord, IgnoredReason, ShouldShowFieldInTableHandler, 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 2b582b965892a..87931e96c4523 100644 --- a/packages/kbn-unified-data-table/src/components/data_table.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.tsx @@ -39,7 +39,11 @@ import { import type { ToastsStart, IUiSettingsClient } from '@kbn/core/public'; import type { Serializable } from '@kbn/utility-types'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { getShouldShowFieldHandler } from '@kbn/discover-utils'; +import { + getShouldShowFieldHandler, + canPrependTimeFieldColumn, + getVisibleColumns, +} from '@kbn/discover-utils'; import type { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { ThemeServiceStart } from '@kbn/react-kibana-context-common'; @@ -62,8 +66,6 @@ import { getRenderCellValueFn } from '../utils/get_render_cell_value'; import { getEuiGridColumns, getLeadControlColumns, - getVisibleColumns, - canPrependTimeFieldColumn, SELECT_ROW, OPEN_DETAILS, } from './data_table_columns'; diff --git a/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx b/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx index 23fcd85de1020..4853201af4b48 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx @@ -8,17 +8,9 @@ */ import React from 'react'; -import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import type { DatatableColumnType } from '@kbn/expressions-plugin/common'; -import { - deserializeHeaderRowHeight, - getEuiGridColumns, - getVisibleColumns, - canPrependTimeFieldColumn, -} from './data_table_columns'; +import { getVisibleColumns } from '@kbn/discover-utils'; +import { deserializeHeaderRowHeight, getEuiGridColumns } from './data_table_columns'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; -import { dataViewWithoutTimefieldMock } from '../../__mocks__/data_view_without_timefield'; import { dataTableContextMock } from '../../__mocks__/table_context'; import { servicesMock } from '../../__mocks__/services'; import { ROWS_HEIGHT_OPTIONS } from '../constants'; @@ -180,179 +172,6 @@ describe('Data table columns', function () { }); }); - describe('getVisibleColumns', () => { - it('returns grid columns without time column when data view has no timestamp field', () => { - const actual = getVisibleColumns(['extension', 'message'], dataViewMock, true) as string[]; - expect(actual).toEqual(['extension', 'message']); - }); - - it('returns grid columns without time column when showTimeCol is falsy', () => { - const actual = getVisibleColumns( - ['extension', 'message'], - dataViewWithTimefieldMock, - false - ) as string[]; - expect(actual).toEqual(['extension', 'message']); - }); - - it('returns grid columns with time column when data view has timestamp field', () => { - const actual = getVisibleColumns( - ['extension', 'message'], - dataViewWithTimefieldMock, - true - ) as string[]; - expect(actual).toEqual(['timestamp', 'extension', 'message']); - }); - }); - - describe('canPrependTimeFieldColumn', () => { - function buildColumnTypes(dataView: DataView) { - const columnsMeta: Record< - string, - { type: DatatableColumnType; esType?: string | undefined } - > = {}; - for (const field of dataView.fields) { - columnsMeta[field.name] = { type: field.type as DatatableColumnType }; - } - return columnsMeta; - } - - describe('dataView with timeField', () => { - it('should forward showTimeCol if no _source columns is passed', () => { - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['extension', 'message'], - dataViewWithTimefieldMock.timeFieldName, - buildColumnTypes(dataViewWithTimefieldMock), - showTimeCol, - false - ) - ).toBe(showTimeCol); - } - }); - - it('should return false if no _source columns is passed, text-based datasource', () => { - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['extension', 'message'], - dataViewWithTimefieldMock.timeFieldName, - buildColumnTypes(dataViewWithTimefieldMock), - showTimeCol, - true - ) - ).toBe(false); - } - }); - - it('should forward showTimeCol if _source column is passed', () => { - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['_source'], - dataViewWithTimefieldMock.timeFieldName, - buildColumnTypes(dataViewWithTimefieldMock), - showTimeCol, - false - ) - ).toBe(showTimeCol); - } - }); - - it('should forward showTimeCol if _source column is passed, text-based datasource', () => { - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['_source'], - dataViewWithTimefieldMock.timeFieldName, - buildColumnTypes(dataViewWithTimefieldMock), - showTimeCol, - true - ) - ).toBe(showTimeCol); - } - }); - - it('should return false if _source column is passed but time field is not returned, text-based datasource', () => { - // ... | DROP @timestamp test case - const columnsMeta = buildColumnTypes(dataViewWithTimefieldMock); - if (dataViewWithTimefieldMock.timeFieldName) { - delete columnsMeta[dataViewWithTimefieldMock.timeFieldName]; - } - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['_source'], - dataViewWithTimefieldMock.timeFieldName, - columnsMeta, - showTimeCol, - true - ) - ).toBe(false); - } - }); - }); - - describe('dataView without timeField', () => { - it('should return false if no _source columns is passed', () => { - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['extension', 'message'], - dataViewWithoutTimefieldMock.timeFieldName, - buildColumnTypes(dataViewWithoutTimefieldMock), - showTimeCol, - false - ) - ).toBe(false); - } - }); - - it('should return false if no _source columns is passed, text-based datasource', () => { - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['extension', 'message'], - dataViewWithoutTimefieldMock.timeFieldName, - buildColumnTypes(dataViewWithoutTimefieldMock), - showTimeCol, - true - ) - ).toBe(false); - } - }); - - it('should return false if _source column is passed', () => { - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['_source'], - dataViewWithoutTimefieldMock.timeFieldName, - buildColumnTypes(dataViewWithoutTimefieldMock), - showTimeCol, - false - ) - ).toBe(false); - } - }); - - it('should return false if _source column is passed, text-based datasource', () => { - for (const showTimeCol of [true, false]) { - expect( - canPrependTimeFieldColumn( - ['_source'], - dataViewWithoutTimefieldMock.timeFieldName, - buildColumnTypes(dataViewWithoutTimefieldMock), - showTimeCol, - true - ) - ).toBe(false); - } - }); - }); - }); - describe('column tokens', () => { it('returns eui grid columns with tokens', async () => { const actual = getEuiGridColumns({ diff --git a/packages/kbn-unified-data-table/src/components/data_table_columns.tsx b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx index 10f55431faa71..4ef0636093f8d 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_columns.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx @@ -358,39 +358,3 @@ export function getEuiGridColumns({ }) ); } - -export function canPrependTimeFieldColumn( - columns: string[], - timeFieldName: string | undefined, - columnsMeta: DataTableColumnsMeta | undefined, - showTimeCol: boolean, - isPlainRecord: boolean -) { - if (!showTimeCol || !timeFieldName) { - return false; - } - - if (isPlainRecord) { - return !!columnsMeta && timeFieldName in columnsMeta && columns.includes('_source'); - } - - return true; -} - -export function getVisibleColumns( - columns: string[], - dataView: DataView, - shouldPrependTimeFieldColumn: boolean -) { - const timeFieldName = dataView.timeFieldName; - - if ( - shouldPrependTimeFieldColumn && - timeFieldName && - !columns.find((col) => col === timeFieldName) - ) { - return [timeFieldName, ...columns]; - } - - return columns; -} diff --git a/packages/kbn-unified-data-table/src/types.ts b/packages/kbn-unified-data-table/src/types.ts index a020a896130f3..77feaf8a5ef44 100644 --- a/packages/kbn-unified-data-table/src/types.ts +++ b/packages/kbn-unified-data-table/src/types.ts @@ -18,8 +18,8 @@ import type { DataTableRecord } from '@kbn/discover-utils/src/types'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { EuiDataGridControlColumn } from '@elastic/eui/src/components/datagrid/data_grid_types'; -import type { DatatableColumnMeta } from '@kbn/expressions-plugin/common'; +export type { DataTableColumnsMeta } from '@kbn/discover-utils/src/types'; export type { DataGridDensity } from './constants'; /** @@ -45,17 +45,6 @@ export type ValueToStringConverter = ( options?: { compatibleWithCSV?: boolean } ) => { formattedString: string; withFormula: boolean }; -/** - * Custom column types per column name - */ -export type DataTableColumnsMeta = Record< - string, - { - type: DatatableColumnMeta['type']; - esType?: DatatableColumnMeta['esType']; - } ->; - export type DataGridCellValueElementProps = EuiDataGridCellValueElementProps & { row: DataTableRecord; dataView: DataView; diff --git a/packages/kbn-unified-doc-viewer/src/services/types.ts b/packages/kbn-unified-doc-viewer/src/services/types.ts index 88b5824c6837f..04d88ba256a37 100644 --- a/packages/kbn-unified-doc-viewer/src/services/types.ts +++ b/packages/kbn-unified-doc-viewer/src/services/types.ts @@ -9,8 +9,11 @@ import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import type { AggregateQuery, Query } from '@kbn/es-query'; -import type { DataTableRecord, IgnoredReason } from '@kbn/discover-utils/types'; -import type { DatatableColumnMeta } from '@kbn/expressions-plugin/common'; +import type { + DataTableRecord, + DataTableColumnsMeta, + IgnoredReason, +} from '@kbn/discover-utils/types'; import { DocViewsRegistry } from './doc_views_registry'; export interface FieldMapping { @@ -36,13 +39,7 @@ export interface DocViewRenderProps { * If not provided, types will be derived by default from the dataView field types. * For displaying text-based search results, define column types (which are available separately in the fetch request) here. */ - columnsMeta?: Record< - string, - { - type: DatatableColumnMeta['type']; - esType?: DatatableColumnMeta['esType']; - } - >; + columnsMeta?: DataTableColumnsMeta; query?: Query | AggregateQuery; textBasedHits?: DataTableRecord[]; hideActionsColumn?: boolean; diff --git a/packages/kbn-unified-doc-viewer/tsconfig.json b/packages/kbn-unified-doc-viewer/tsconfig.json index 05bb4f1539598..e9429af74bd74 100644 --- a/packages/kbn-unified-doc-viewer/tsconfig.json +++ b/packages/kbn-unified-doc-viewer/tsconfig.json @@ -24,6 +24,5 @@ "@kbn/i18n", "@kbn/react-field", "@kbn/field-utils", - "@kbn/expressions-plugin", ] } diff --git a/src/plugins/esql_datagrid/public/data_grid.tsx b/src/plugins/esql_datagrid/public/data_grid.tsx index 990d27891bf6c..58145627f139f 100644 --- a/src/plugins/esql_datagrid/public/data_grid.tsx +++ b/src/plugins/esql_datagrid/public/data_grid.tsx @@ -21,10 +21,10 @@ import { EuiLink, EuiText, EuiIcon } from '@elastic/eui'; import { css } from '@emotion/react'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { ESQLRow } from '@kbn/es-types'; -import type { DatatableColumn, DatatableColumnMeta } from '@kbn/expressions-plugin/common'; +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { AggregateQuery } from '@kbn/es-query'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { DataTableRecord, DataTableColumnsMeta } from '@kbn/discover-utils/types'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { CoreStart } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -46,13 +46,6 @@ interface ESQLDataGridProps { initialRowHeight?: number; controlColumnIds?: string[]; } -type DataTableColumnsMeta = Record< - string, - { - type: DatatableColumnMeta['type']; - esType?: DatatableColumnMeta['esType']; - } ->; const sortOrder: SortOrder[] = []; const DEFAULT_INITIAL_ROW_HEIGHT = 5; diff --git a/src/plugins/esql_datagrid/public/row_viewer.tsx b/src/plugins/esql_datagrid/public/row_viewer.tsx index 8be9a67b36894..2578bee01e6cb 100644 --- a/src/plugins/esql_datagrid/public/row_viewer.tsx +++ b/src/plugins/esql_datagrid/public/row_viewer.tsx @@ -9,8 +9,7 @@ import React, { useMemo } from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; -import type { DataTableColumnsMeta } from '@kbn/unified-data-table'; +import type { DataTableRecord, DataTableColumnsMeta } from '@kbn/discover-utils/types'; import { UnifiedDocViewerFlyout } from '@kbn/unified-doc-viewer-plugin/public'; import { NotificationsStart } from '@kbn/core-notifications-browser'; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_flyout/doc_viewer_flyout.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_flyout/doc_viewer_flyout.tsx index 754f9c4c05b6e..540c04a206c61 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_flyout/doc_viewer_flyout.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_flyout/doc_viewer_flyout.tsx @@ -29,8 +29,7 @@ import { useIsWithinMinBreakpoint, EuiFlyoutProps, } from '@elastic/eui'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; -import type { DataTableColumnsMeta } from '@kbn/unified-data-table'; +import type { DataTableRecord, DataTableColumnsMeta } from '@kbn/discover-utils/types'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import type { ToastsStart } from '@kbn/core-notifications-browser'; import type { DocViewFilterFn, DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.test.tsx new file mode 100644 index 0000000000000..833b9db059975 --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.test.tsx @@ -0,0 +1,168 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { buildDataTableRecord } from '@kbn/discover-utils'; +import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { generateEsHits } from '@kbn/discover-utils/src/__mocks__'; +import { DocViewerTable, SHOW_ONLY_SELECTED_FIELDS } from './table'; +import { mockUnifiedDocViewerServices } from '../../__mocks__'; +import { setUnifiedDocViewerServices } from '../../plugin'; + +const storage = new Storage(window.localStorage); + +setUnifiedDocViewerServices(mockUnifiedDocViewerServices); + +const dataView = createStubDataView({ + spec: { + id: 'test', + title: 'test', + timeFieldName: '@timestamp', + fields: { + '@timestamp': { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + count: 30, + readFromDocValues: true, + scripted: false, + isMapped: true, + }, + bytes: { + name: 'bytes', + type: 'number', + esTypes: ['long'], + aggregatable: true, + searchable: true, + count: 10, + readFromDocValues: true, + scripted: false, + isMapped: true, + }, + 'extension.keyword': { + name: 'extension.keyword', + type: 'string', + esTypes: ['keyword'], + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: true, + scripted: false, + subType: { + multi: { + parent: 'extension', + }, + }, + isMapped: true, + }, + _id: { + name: '_id', + type: 'string', + esTypes: ['_id'], + aggregatable: false, + searchable: true, + readFromDocValues: true, + isMapped: true, + }, + }, + }, +}); +const hit = buildDataTableRecord(generateEsHits(dataView, 1)[0], dataView); + +describe('DocViewerTable', () => { + afterEach(() => { + storage.clear(); + }); + + describe('switch - show only selected fields', () => { + it('should disable the switch if columns is empty', async () => { + render( + + + + ); + + expect(screen.getByTestId('unifiedDocViewerShowOnlySelectedFieldsSwitch')).toBeDisabled(); + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.getByText('bytes')).toBeInTheDocument(); + expect(screen.getByText('extension.keyword')).toBeInTheDocument(); + }); + + it('should disable the switch even if it was previously switched on', async () => { + storage.set(SHOW_ONLY_SELECTED_FIELDS, true); + + render( + + + + ); + + expect(screen.getByTestId('unifiedDocViewerShowOnlySelectedFieldsSwitch')).toBeDisabled(); + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.getByText('bytes')).toBeInTheDocument(); + expect(screen.getByText('extension.keyword')).toBeInTheDocument(); + }); + + it('should show only selected fields if it was previously switched on', async () => { + storage.set(SHOW_ONLY_SELECTED_FIELDS, true); + + render( + + + + ); + + expect(screen.getByTestId('unifiedDocViewerShowOnlySelectedFieldsSwitch')).toBeEnabled(); + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.queryByText('bytes')).toBeNull(); + expect(screen.getByText('extension.keyword')).toBeInTheDocument(); + }); + + it('should allow toggling the switch', async () => { + render( + + + + ); + + const showOnlySelectedFieldsSwitch = screen.getByTestId( + 'unifiedDocViewerShowOnlySelectedFieldsSwitch' + ); + + expect(showOnlySelectedFieldsSwitch).toBeEnabled(); + expect(showOnlySelectedFieldsSwitch).toHaveValue(''); + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.getByText('bytes')).toBeInTheDocument(); + expect(screen.getByText('extension.keyword')).toBeInTheDocument(); + + act(() => { + showOnlySelectedFieldsSwitch.click(); + }); + + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.getByText('bytes')).toBeInTheDocument(); + expect(screen.queryByText('extension.keyword')).toBeNull(); + expect(storage.get(SHOW_ONLY_SELECTED_FIELDS)).toBe(true); + + act(() => { + showOnlySelectedFieldsSwitch.click(); + }); + + expect(screen.getByText('@timestamp')).toBeInTheDocument(); + expect(screen.getByText('bytes')).toBeInTheDocument(); + expect(screen.getByText('extension.keyword')).toBeInTheDocument(); + expect(storage.get(SHOW_ONLY_SELECTED_FIELDS)).toBe(false); + }); + }); +}); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx index 76b1e0b02940d..e542a94c5fca8 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx @@ -24,7 +24,6 @@ import { EuiCallOut, useResizeObserver, EuiSwitch, - useEuiTheme, EuiSwitchEvent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -33,11 +32,14 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; import { getFieldIconType } from '@kbn/field-utils/src/utils/get_field_icon_type'; import { SHOW_MULTIFIELDS, + DOC_HIDE_TIME_COLUMN_SETTING, formatFieldValue, getIgnoredReason, getShouldShowFieldHandler, isNestedFieldParent, usePager, + getVisibleColumns, + canPrependTimeFieldColumn, } from '@kbn/discover-utils'; import { getTextBasedColumnIconType } from '@kbn/field-utils'; import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; @@ -72,6 +74,7 @@ const DEFAULT_PAGE_SIZE = 25; const PINNED_FIELDS_KEY = 'discover:pinnedFields'; const PAGE_SIZE = 'discover:pageSize'; const HIDE_NULL_VALUES = 'unifiedDocViewer:hideNullValues'; +export const SHOW_ONLY_SELECTED_FIELDS = 'unifiedDocViewer:showOnlySelectedFields'; const GRID_COLUMN_FIELD_NAME = 'name'; const GRID_COLUMN_FIELD_VALUE = 'value'; @@ -143,8 +146,10 @@ export const DocViewerTable = ({ getPinnedFields(currentDataViewId, storage) ); const [areNullValuesHidden, setAreNullValuesHidden] = useLocalStorage(HIDE_NULL_VALUES, false); - - const { euiTheme } = useEuiTheme(); + const [showOnlySelectedFields, setShowOnlySelectedFields] = useLocalStorage( + SHOW_ONLY_SELECTED_FIELDS, + false + ); const flattened = hit.flattened; const shouldShowFieldHandler = useMemo( @@ -237,59 +242,97 @@ export const DocViewerTable = ({ ] ); + const fieldsFromColumns = useMemo( + () => columns?.filter((column) => column !== '_source') || [], + [columns] + ); + + const isShowOnlySelectedFieldsDisabled = !fieldsFromColumns?.length; + + const shouldShowOnlySelectedFields = useMemo( + () => showOnlySelectedFields && !isShowOnlySelectedFieldsDisabled, + [showOnlySelectedFields, isShowOnlySelectedFieldsDisabled] + ); + + const displayedFieldNames = useMemo(() => { + if (shouldShowOnlySelectedFields) { + return getVisibleColumns( + fieldsFromColumns, + dataView, + canPrependTimeFieldColumn( + columns, + dataView.timeFieldName, + columnsMeta, + !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), + isEsqlMode + ) + ); + } + return Object.keys(flattened).sort((fieldA, fieldB) => { + const mappingA = mapping(fieldA); + const mappingB = mapping(fieldB); + const nameA = !mappingA || !mappingA.displayName ? fieldA : mappingA.displayName; + const nameB = !mappingB || !mappingB.displayName ? fieldB : mappingB.displayName; + return nameA.localeCompare(nameB); + }); + }, [ + fieldsFromColumns, + flattened, + shouldShowOnlySelectedFields, + mapping, + dataView, + columns, + columnsMeta, + isEsqlMode, + uiSettings, + ]); + const { pinnedItems, restItems, allFields } = useMemo( () => - Object.keys(flattened) - .sort((fieldA, fieldB) => { - const mappingA = mapping(fieldA); - const mappingB = mapping(fieldB); - const nameA = !mappingA || !mappingA.displayName ? fieldA : mappingA.displayName; - const nameB = !mappingB || !mappingB.displayName ? fieldB : mappingB.displayName; - return nameA.localeCompare(nameB); - }) - .reduce( - (acc, curFieldName) => { - if (!shouldShowFieldHandler(curFieldName)) { - return acc; - } - const shouldHideNullValue = - areNullValuesHidden && flattened[curFieldName] == null && isEsqlMode; - if (shouldHideNullValue) { - return acc; - } + displayedFieldNames.reduce( + (acc, curFieldName) => { + if (!shouldShowOnlySelectedFields && !shouldShowFieldHandler(curFieldName)) { + return acc; + } + const shouldHideNullValue = + isEsqlMode && areNullValuesHidden && flattened[curFieldName] == null; + if (shouldHideNullValue) { + return acc; + } - const isPinned = pinnedFields.includes(curFieldName); - const row = fieldToItem(curFieldName, isPinned); + const isPinned = pinnedFields.includes(curFieldName); + const row = fieldToItem(curFieldName, isPinned); - if (isPinned) { - acc.pinnedItems.push(row); - } else { - if (onFilterField(curFieldName, row.field.displayName, row.field.fieldType)) { - // filter only unpinned fields - acc.restItems.push(row); - } + if (isPinned) { + acc.pinnedItems.push(row); + } else { + if (onFilterField(curFieldName, row.field.displayName, row.field.fieldType)) { + // filter only unpinned fields + acc.restItems.push(row); } + } - acc.allFields.push({ - name: curFieldName, - displayName: row.field.displayName, - type: row.field.fieldType, - }); + acc.allFields.push({ + name: curFieldName, + displayName: row.field.displayName, + type: row.field.fieldType, + }); - return acc; - }, - { - pinnedItems: [], - restItems: [], - allFields: [], - } - ), + return acc; + }, + { + pinnedItems: [], + restItems: [], + allFields: [], + } + ), [ + displayedFieldNames, areNullValuesHidden, + shouldShowOnlySelectedFields, fieldToItem, flattened, isEsqlMode, - mapping, onFilterField, pinnedFields, shouldShowFieldHandler, @@ -375,6 +418,13 @@ export const DocViewerTable = ({ [setAreNullValuesHidden] ); + const onShowOnlySelectedFieldsChange = useCallback( + (e: EuiSwitchEvent) => { + setShowOnlySelectedFields(e.target.checked); + }, + [setShowOnlySelectedFields] + ); + const renderCellValue: EuiDataGridProps['renderCellValue'] = useCallback( ({ rowIndex, columnId, isDetails }) => { return ( @@ -446,31 +496,38 @@ export const DocViewerTable = ({ - {rows.length === 0 ? ( - -

- -

-
- ) : ( - <> + + + + + + - + {isEsqlMode && ( - + )} - - + + + + + + + {rows.length === 0 ? ( + +

+ - - +

+
+ ) : ( + + + )}
); diff --git a/src/plugins/unified_doc_viewer/tsconfig.json b/src/plugins/unified_doc_viewer/tsconfig.json index ef3a7a91153ac..eab6884b972ec 100644 --- a/src/plugins/unified_doc_viewer/tsconfig.json +++ b/src/plugins/unified_doc_viewer/tsconfig.json @@ -31,7 +31,6 @@ "@kbn/ui-theme", "@kbn/discover-shared-plugin", "@kbn/fields-metadata-plugin", - "@kbn/unified-data-table", "@kbn/core-notifications-browser", "@kbn/deeplinks-observability", "@kbn/share-plugin", diff --git a/test/functional/apps/discover/group3/_doc_viewer.ts b/test/functional/apps/discover/group3/_doc_viewer.ts index 6da0725978afe..3d3562e10beb4 100644 --- a/test/functional/apps/discover/group3/_doc_viewer.ts +++ b/test/functional/apps/discover/group3/_doc_viewer.ts @@ -233,6 +233,151 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); + describe('show only selected fields in ES|QL mode', function () { + beforeEach(async () => { + await discover.selectTextBaseLang(); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + }); + + it('should disable the switch when no fields are selected', async function () { + const testQuery = 'from logstash-* | sort @timestamp | limit 10'; + await monacoEditor.setCodeEditorValue(testQuery); + await testSubjects.click('querySubmitButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await dataGrid.clickRowToggle(); + await discover.isShowingDocViewer(); + + const showOnlySelectedFieldsSwitch = await testSubjects.find( + 'unifiedDocViewerShowOnlySelectedFieldsSwitch' + ); + expect(await showOnlySelectedFieldsSwitch.getAttribute('disabled')).to.be('true'); + + const fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + const fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + + expect( + fieldNames.join(',').startsWith('@message,@tags,@timestamp,agent,bytes,clientip') + ).to.be(true); + }); + + it('should allow toggling the switch', async function () { + const testQuery = 'from logstash-* | sort @timestamp | limit 10'; + await monacoEditor.setCodeEditorValue(testQuery); + await testSubjects.click('querySubmitButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + await unifiedFieldList.clickFieldListItemAdd('agent.raw'); + await header.waitUntilLoadingHasFinished(); + await unifiedFieldList.clickFieldListItemAdd('agent'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + await dataGrid.clickRowToggle(); + await discover.isShowingDocViewer(); + + const showOnlySelectedFieldsSwitch = await testSubjects.find( + 'unifiedDocViewerShowOnlySelectedFieldsSwitch' + ); + expect(await showOnlySelectedFieldsSwitch.getAttribute('disabled')).to.be(null); + + let fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + let fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + + expect( + fieldNames.join(',').startsWith('@message,@tags,@timestamp,agent,bytes,clientip') + ).to.be(true); + + await showOnlySelectedFieldsSwitch.click(); + + await retry.waitFor('updates after switching to show only selected', async () => { + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + return fieldNames.join(',') === 'agent.raw,agent'; + }); + + await dataGrid.togglePinActionInFlyout('agent'); + + await retry.waitFor('updates after pinning the last field', async () => { + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + return fieldNames.join(',') === 'agent,agent.raw'; + }); + + await showOnlySelectedFieldsSwitch.click(); + + await retry.waitFor('updates after switching from showing only selected', async () => { + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + return fieldNames.join(',').startsWith('agent,@message,@tags'); + }); + }); + }); + + describe('show only selected fields in data view mode', function () { + it('should disable the switch when no fields are selected', async function () { + await dataGrid.clickRowToggle(); + await discover.isShowingDocViewer(); + + const showOnlySelectedFieldsSwitch = await testSubjects.find( + 'unifiedDocViewerShowOnlySelectedFieldsSwitch' + ); + expect(await showOnlySelectedFieldsSwitch.getAttribute('disabled')).to.be('true'); + + const fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + const fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + + expect(fieldNames.join(',').startsWith('_id,_ignored,_index,_score,@message')).to.be(true); + }); + + it('should allow toggling the switch', async function () { + await unifiedFieldList.clickFieldListItemAdd('bytes'); + await header.waitUntilLoadingHasFinished(); + await unifiedFieldList.clickFieldListItemAdd('@tags'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + await dataGrid.clickRowToggle(); + await discover.isShowingDocViewer(); + + const showOnlySelectedFieldsSwitch = await testSubjects.find( + 'unifiedDocViewerShowOnlySelectedFieldsSwitch' + ); + expect(await showOnlySelectedFieldsSwitch.getAttribute('disabled')).to.be(null); + + let fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + let fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + + expect(fieldNames.join(',').startsWith('_id,_ignored,_index,_score,@message')).to.be(true); + + await showOnlySelectedFieldsSwitch.click(); + + await retry.waitFor('updates after switching to show only selected', async () => { + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + return fieldNames.join(',') === '@timestamp,bytes,@tags'; + }); + + await dataGrid.togglePinActionInFlyout('bytes'); + + await retry.waitFor('updates after pinning the last field', async () => { + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + return fieldNames.join(',') === 'bytes,@timestamp,@tags'; + }); + + await showOnlySelectedFieldsSwitch.click(); + + await retry.waitFor('updates after switching from showing only selected', async () => { + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + return fieldNames.join(',').startsWith('bytes,_id,_ignored,_index,_score,@message'); + }); + }); + }); + describe('pinning fields', function () { it('should be able to pin and unpin fields', async function () { await dataGrid.clickRowToggle();