diff --git a/src/platform/packages/shared/kbn-data-view-utils/index.ts b/src/platform/packages/shared/kbn-data-view-utils/index.ts index ae013a12a4715..12430e0bccf12 100644 --- a/src/platform/packages/shared/kbn-data-view-utils/index.ts +++ b/src/platform/packages/shared/kbn-data-view-utils/index.ts @@ -9,5 +9,6 @@ export * from './src/constants'; export { convertDatatableColumnToDataViewFieldSpec } from './src/utils/convert_to_data_view_field_spec'; +export { getDataViewFieldOrCreateFromColumnMeta } from './src/utils/get_data_view_field_or_create'; export { createRegExpPatternFrom } from './src/utils/create_regexp_pattern_from'; export { testPatternAgainstAllowedList } from './src/utils/test_pattern_against_allowed_list'; diff --git a/src/platform/packages/shared/kbn-data-view-utils/src/utils/get_data_view_field_or_create.ts b/src/platform/packages/shared/kbn-data-view-utils/src/utils/get_data_view_field_or_create.ts new file mode 100644 index 0000000000000..620cd8841d6b4 --- /dev/null +++ b/src/platform/packages/shared/kbn-data-view-utils/src/utils/get_data_view_field_or_create.ts @@ -0,0 +1,45 @@ +/* + * 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 { isEqual } from 'lodash'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DatatableColumnMeta } from '@kbn/expressions-plugin/common'; +import { convertDatatableColumnToDataViewFieldSpec } from './convert_to_data_view_field_spec'; + +export const getDataViewFieldOrCreateFromColumnMeta = ({ + dataView, + fieldName, + columnMeta, +}: { + dataView: DataView; + fieldName: string; + columnMeta?: DatatableColumnMeta; // based on ES|QL query +}) => { + const dataViewField = dataView.fields.getByName(fieldName); + + if (!columnMeta) { + return dataViewField; + } + + const fieldSpecFromColumnMeta = convertDatatableColumnToDataViewFieldSpec({ + name: fieldName, + id: fieldName, + meta: columnMeta, + }); + + if ( + !dataViewField || + dataViewField.type !== fieldSpecFromColumnMeta.type || + !isEqual(dataViewField.esTypes, fieldSpecFromColumnMeta.esTypes) + ) { + return dataView.fields.create(fieldSpecFromColumnMeta); + } + + return dataViewField; +}; diff --git a/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/data_view.ts b/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/data_view.ts index daa8835e817c4..b11e2c6c73c2f 100644 --- a/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/data_view.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/data_view.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { DataView, DataViewField, FieldSpec } from '@kbn/data-views-plugin/public'; export const shallowMockedFields = [ { @@ -101,6 +101,10 @@ export const buildDataViewMock = ({ return dataViewFields; }; + dataViewFields.create = (spec: FieldSpec) => { + return new DataViewField(spec); + }; + const dataView = { id: `${name}-id`, title: `${name}-title`, diff --git a/src/platform/packages/shared/kbn-unified-data-table/__mocks__/table_context.ts b/src/platform/packages/shared/kbn-unified-data-table/__mocks__/table_context.ts index 739e04a954e07..d5aebf3c9a3b5 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/__mocks__/table_context.ts +++ b/src/platform/packages/shared/kbn-unified-data-table/__mocks__/table_context.ts @@ -36,6 +36,7 @@ const buildTableContext = (dataView: DataView, rows: DataTableRecord[]): DataTab fieldFormats: servicesMock.fieldFormats, rows, dataView, + columnsMeta: undefined, options, }), }; diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/components/__snapshots__/data_table_columns.test.tsx.snap b/src/platform/packages/shared/kbn-unified-data-table/src/components/__snapshots__/data_table_columns.test.tsx.snap index fd1ad71558aa5..3f52dc584a83e 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/components/__snapshots__/data_table_columns.test.tsx.snap +++ b/src/platform/packages/shared/kbn-unified-data-table/src/components/__snapshots__/data_table_columns.test.tsx.snap @@ -164,150 +164,7 @@ Array [ ], "getAllowHidden": [Function], "getComputedFields": [Function], - "getFieldByName": [MockFunction] { - "calls": Array [ - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - ], - }, + "getFieldByName": [MockFunction], "getFormatterForField": [MockFunction], "getIndexPattern": [Function], "getName": [Function], @@ -440,123 +297,7 @@ Array [ ], "getAllowHidden": [Function], "getComputedFields": [Function], - "getFieldByName": [MockFunction] { - "calls": Array [ - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - ], - }, + "getFieldByName": [MockFunction], "getFormatterForField": [MockFunction], "getIndexPattern": [Function], "getName": [Function], @@ -701,123 +442,7 @@ Array [ ], "getAllowHidden": [Function], "getComputedFields": [Function], - "getFieldByName": [MockFunction] { - "calls": Array [ - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - ], - }, + "getFieldByName": [MockFunction], "getFormatterForField": [MockFunction], "getIndexPattern": [Function], "getName": [Function], @@ -949,123 +574,7 @@ Array [ ], "getAllowHidden": [Function], "getComputedFields": [Function], - "getFieldByName": [MockFunction] { - "calls": Array [ - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - ], - }, + "getFieldByName": [MockFunction], "getFormatterForField": [MockFunction], "getIndexPattern": [Function], "getName": [Function], @@ -1215,150 +724,7 @@ Array [ ], "getAllowHidden": [Function], "getComputedFields": [Function], - "getFieldByName": [MockFunction] { - "calls": Array [ - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "timestamp", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - Array [ - "extension", - ], - Array [ - "message", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "timestamp", - "filterable": true, - "name": "timestamp", - "scripted": false, - "sortable": true, - "type": "date", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "aggregatable": true, - "displayName": "extension", - "filterable": true, - "name": "extension", - "scripted": false, - "type": "string", - }, - }, - Object { - "type": "return", - "value": Object { - "displayName": "message", - "filterable": false, - "name": "message", - "scripted": false, - "type": "string", - }, - }, - ], - }, + "getFieldByName": [MockFunction], "getFormatterForField": [MockFunction], "getIndexPattern": [Function], "getName": [Function], @@ -1434,6 +800,8 @@ Array [ }, "cellActions": Array [ [Function], + [Function], + [Function], ], "display": number>>( (acc, { id, direction }) => { - const field = dataView.fields.getByName(id); + const field = getDataViewFieldOrCreateFromColumnMeta({ + dataView, + fieldName: id, + columnMeta: columnsMeta?.[id], + }); if (!field) { return acc; } - const sortField = getSortingCriteria( - columnsMeta?.[id]?.type ?? field.type, - id, - dataView.getFormatterForField(field) - ); + const sortField = getSortingCriteria(field.type, id, dataView.getFormatterForField(field)); acc.push((a, b) => sortField(a.flattened, b.flattened, direction as 'asc' | 'desc')); diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/utils/convert_value_to_string.test.tsx b/src/platform/packages/shared/kbn-unified-data-table/src/utils/convert_value_to_string.test.tsx index 749a2b34e579c..14c07a8a8137f 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/utils/convert_value_to_string.test.tsx +++ b/src/platform/packages/shared/kbn-unified-data-table/src/utils/convert_value_to_string.test.tsx @@ -17,6 +17,8 @@ import { servicesMock } from '../../__mocks__/services'; import { convertValueToString, convertNameToString } from './convert_value_to_string'; describe('convertValueToString', () => { + jest.spyOn(dataTableContextComplexMock.dataView.fields, 'create'); + it('should convert a keyword value to text', () => { const result = convertValueToString({ rows: dataTableContextComplexRowsMock, @@ -24,6 +26,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'keyword_key', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -39,6 +42,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'text_message', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -47,50 +51,60 @@ describe('convertValueToString', () => { expect(result.formattedString).toBe('"Hi there! I am a sample string."'); }); - it('should convert a text value to text (not for CSV)', () => { + it('should convert a multiline text value to text', () => { const result = convertValueToString({ rows: dataTableContextComplexRowsMock, dataView: dataTableContextComplexMock.dataView, fieldFormats: servicesMock.fieldFormats, columnId: 'text_message', - rowIndex: 0, + rowIndex: 1, + columnsMeta: undefined, options: { - compatibleWithCSV: false, + compatibleWithCSV: true, }, }); - expect(result.formattedString).toBe('Hi there! I am a sample string.'); + expect(result.formattedString).toBe('"I\'m multiline\n*&%$#@"'); + expect(result.withFormula).toBe(false); }); - it('should convert a multiline text value to text', () => { + it('should convert a number value to text', () => { const result = convertValueToString({ rows: dataTableContextComplexRowsMock, dataView: dataTableContextComplexMock.dataView, fieldFormats: servicesMock.fieldFormats, - columnId: 'text_message', - rowIndex: 1, + columnId: 'number_price', + rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, }); - expect(result.formattedString).toBe('"I\'m multiline\n*&%$#@"'); - expect(result.withFormula).toBe(false); + expect(result.formattedString).toBe('10.99'); + expect(dataTableContextComplexMock.dataView.fields.create).toHaveBeenCalledTimes(0); }); - it('should convert a number value to text', () => { + it('should convert a number value as keyword override to text', () => { const result = convertValueToString({ rows: dataTableContextComplexRowsMock, dataView: dataTableContextComplexMock.dataView, fieldFormats: servicesMock.fieldFormats, columnId: 'number_price', rowIndex: 0, + columnsMeta: { + number_price: { + type: 'string', + esType: 'keyword', + }, + }, options: { compatibleWithCSV: true, }, }); expect(result.formattedString).toBe('10.99'); + expect(dataTableContextComplexMock.dataView.fields.create).toHaveBeenCalledTimes(1); }); it('should convert a date value to text', () => { @@ -100,6 +114,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'date', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -115,6 +130,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'date_nanos', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -130,6 +146,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'date_nanos', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: false, }, @@ -145,6 +162,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'bool_enabled', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -160,6 +178,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'binary_blob', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -175,6 +194,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'binary_blob', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: false, }, @@ -190,6 +210,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'object_user.first', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -205,6 +226,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'nested_user', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -222,6 +244,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'flattened_labels', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -237,6 +260,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'range_time_frame', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -254,6 +278,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'rank_features', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -269,6 +294,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'histogram', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -284,6 +310,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'ip_addr', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -299,6 +326,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'ip_addr', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: false, }, @@ -314,6 +342,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'version', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -329,6 +358,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'version', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: false, }, @@ -344,6 +374,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'vector', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -359,6 +390,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'geo_point', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -374,6 +406,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'geo_point', rowIndex: 1, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -389,6 +422,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'array_tags', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -404,6 +438,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'geometry', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -421,6 +456,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'runtime_number', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -436,6 +472,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'scripted_string', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -451,6 +488,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'scripted_string', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: false, }, @@ -466,6 +504,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'unknown', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -481,6 +520,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'unknown', rowIndex: -1, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -496,6 +536,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: '_source', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: false, }, @@ -519,6 +560,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: '_source', rowIndex: 0, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -536,6 +578,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'array_tags', rowIndex: 1, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -550,6 +593,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'scripted_string', rowIndex: 1, + columnsMeta: undefined, options: { compatibleWithCSV: true, }, @@ -566,6 +610,7 @@ describe('convertValueToString', () => { fieldFormats: servicesMock.fieldFormats, columnId: 'array_tags', rowIndex: 1, + columnsMeta: undefined, options: { compatibleWithCSV: false, }, diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/utils/convert_value_to_string.ts b/src/platform/packages/shared/kbn-unified-data-table/src/utils/convert_value_to_string.ts index c8d09092709ad..d781120c631cf 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/utils/convert_value_to_string.ts +++ b/src/platform/packages/shared/kbn-unified-data-table/src/utils/convert_value_to_string.ts @@ -9,8 +9,9 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import { cellHasFormulas, createEscapeValue } from '@kbn/data-plugin/common'; +import { getDataViewFieldOrCreateFromColumnMeta } from '@kbn/data-view-utils'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { DataTableRecord, DataTableColumnsMeta } from '@kbn/discover-utils/types'; import { formatFieldValue } from '@kbn/discover-utils'; interface ConvertedResult { @@ -26,6 +27,7 @@ export const convertValueToString = ({ columnId, dataView, fieldFormats, + columnsMeta, options, }: { rowIndex: number; @@ -33,6 +35,7 @@ export const convertValueToString = ({ columnId: string; dataView: DataView; fieldFormats: FieldFormatsStart; + columnsMeta: DataTableColumnsMeta | undefined; options?: { compatibleWithCSV?: boolean; // values as one-liner + escaping formulas + adding wrapping quotes }; @@ -45,7 +48,11 @@ export const convertValueToString = ({ } const rowFlattened = rows[rowIndex].flattened; const value = rowFlattened?.[columnId]; - const field = dataView.fields.getByName(columnId); + const field = getDataViewFieldOrCreateFromColumnMeta({ + fieldName: columnId, + dataView, + columnMeta: columnsMeta?.[columnId], + }); const valuesArray = Array.isArray(value) ? value : [value]; const disableMultiline = options?.compatibleWithCSV ?? false; const enableEscapingForValue = options?.compatibleWithCSV ?? false; diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/utils/copy_value_to_clipboard.test.tsx b/src/platform/packages/shared/kbn-unified-data-table/src/utils/copy_value_to_clipboard.test.tsx index 86675c5fbc707..e841900b2de2b 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/utils/copy_value_to_clipboard.test.tsx +++ b/src/platform/packages/shared/kbn-unified-data-table/src/utils/copy_value_to_clipboard.test.tsx @@ -33,6 +33,7 @@ describe('copyValueToClipboard', () => { fieldFormats: servicesMock.fieldFormats, rowIndex, columnId, + columnsMeta: undefined, options, }); diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx b/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx index 0dfeb1f691e88..960a576f8342f 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx +++ b/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.test.tsx @@ -117,6 +117,7 @@ describe('Unified data table cell rendering', function () { closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, + columnsMeta: undefined, }); const component = shallow( @@ -669,6 +682,7 @@ describe('Unified data table cell rendering', function () { closePopover: jest.fn(), fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, maxEntries: 100, + columnsMeta: undefined, }); const component = shallow( `); }); + + it('renders custom ES|QL fields correctly', () => { + jest.spyOn(dataViewMock.fields, 'create'); + + const rows: EsHitRecord[] = [ + { + _id: '1', + _index: 'test', + _score: 1, + _source: undefined, + fields: { bytes: 100, var0: 350, extension: 'gif' }, + }, + ]; + const DataTableCellValue = getRenderCellValueFn({ + dataView: dataViewMock, + rows: rows.map(build), + shouldShowFieldHandler: () => true, + closePopover: jest.fn(), + fieldFormats: mockServices.fieldFormats as unknown as FieldFormatsStart, + maxEntries: 100, + columnsMeta: { + // custom ES|QL var + var0: { + type: 'number', + esType: 'long', + }, + // custom ES|QL override + bytes: { + type: 'string', + esType: 'keyword', + }, + }, + }); + const componentWithDataViewField = shallow( + + ); + expect(componentWithDataViewField).toMatchInlineSnapshot(` + + `); + const componentWithCustomESQLField = shallow( + + ); + expect(componentWithCustomESQLField).toMatchInlineSnapshot(` + + `); + + expect(dataViewMock.fields.create).toHaveBeenCalledTimes(1); + expect(dataViewMock.fields.create).toHaveBeenCalledWith({ + name: 'var0', + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: false, + isNull: false, + }); + + const componentWithCustomESQLFieldOverride = shallow( + + ); + expect(componentWithCustomESQLFieldOverride).toMatchInlineSnapshot(` + + `); + + expect(dataViewMock.fields.create).toHaveBeenCalledTimes(2); + expect(dataViewMock.fields.create).toHaveBeenLastCalledWith({ + name: 'bytes', + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: false, + isNull: false, + }); + }); }); diff --git a/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.tsx b/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.tsx index 117dd122b8a45..499e5425ed3e5 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.tsx +++ b/src/platform/packages/shared/kbn-unified-data-table/src/utils/get_render_cell_value.tsx @@ -17,7 +17,12 @@ import { EuiDataGridCellValueElementProps, } from '@elastic/eui'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import type { DataTableRecord, ShouldShowFieldInTableHandler } from '@kbn/discover-utils/types'; +import { getDataViewFieldOrCreateFromColumnMeta } from '@kbn/data-view-utils'; +import { + DataTableColumnsMeta, + DataTableRecord, + ShouldShowFieldInTableHandler, +} from '@kbn/discover-utils/types'; import { formatFieldValue } from '@kbn/discover-utils'; import { UnifiedDataTableContext } from '../table_context'; import type { CustomCellRenderer } from '../types'; @@ -39,6 +44,7 @@ export const getRenderCellValueFn = ({ externalCustomRenderers, isPlainRecord, isCompressed = true, + columnsMeta, }: { dataView: DataView; rows: DataTableRecord[] | undefined; @@ -49,6 +55,7 @@ export const getRenderCellValueFn = ({ externalCustomRenderers?: CustomCellRenderer; isPlainRecord?: boolean; isCompressed?: boolean; + columnsMeta: DataTableColumnsMeta | undefined; }) => { const UnifiedDataTableRenderCellValue = ({ rowIndex, @@ -60,7 +67,11 @@ export const getRenderCellValueFn = ({ isExpanded, }: EuiDataGridCellValueElementProps) => { const row = rows ? rows[rowIndex] : undefined; - const field = dataView.fields.getByName(columnId); + const field = getDataViewFieldOrCreateFromColumnMeta({ + dataView, + fieldName: columnId, + columnMeta: columnsMeta?.[columnId], + }); const ctx = useContext(UnifiedDataTableContext); useEffect(() => { diff --git a/src/platform/packages/shared/kbn-unified-data-table/tsconfig.json b/src/platform/packages/shared/kbn-unified-data-table/tsconfig.json index c3840e246552d..fe6d8fea644aa 100644 --- a/src/platform/packages/shared/kbn-unified-data-table/tsconfig.json +++ b/src/platform/packages/shared/kbn-unified-data-table/tsconfig.json @@ -44,5 +44,6 @@ "@kbn/core-capabilities-browser-mocks", "@kbn/sort-predicates", "@kbn/data-grid-in-table-search", + "@kbn/data-view-utils", ] } diff --git a/src/platform/plugins/shared/data_views/common/fields/field_list.ts b/src/platform/plugins/shared/data_views/common/fields/field_list.ts index 11be41fcebce3..7e7c6ab88fadb 100644 --- a/src/platform/plugins/shared/data_views/common/fields/field_list.ts +++ b/src/platform/plugins/shared/data_views/common/fields/field_list.ts @@ -22,6 +22,12 @@ interface ToSpecOptions { * Interface for data view field list which _extends_ the array class. */ export interface IIndexPatternFieldList extends Array { + /** + * Creates a DataViewField instance. Does not add it to the data view. + * @param field field spec to create field instance + * @returns a new data view field instance + */ + create(field: FieldSpec): DataViewField; /** * Add field to field list. * @param field field spec to add field to list @@ -101,8 +107,13 @@ export const fieldList = ( public readonly getByType = (type: DataViewField['type']) => [ ...(this.groups.get(type) || new Map()).values(), ]; + + public readonly create = (field: FieldSpec): DataViewField => { + return new DataViewField({ ...field, shortDotsEnable }); + }; + public readonly add = (field: FieldSpec): DataViewField => { - const newField = new DataViewField({ ...field, shortDotsEnable }); + const newField = this.create(field); this.push(newField); this.setByName(newField); this.setByGroup(newField); diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_table/field_row.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_table/field_row.ts index d020692bf4871..8d3aedd40122f 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_table/field_row.ts +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/doc_viewer_table/field_row.ts @@ -17,6 +17,7 @@ import { } from '@kbn/discover-utils'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { getFieldIconType, getTextBasedColumnIconType } from '@kbn/field-utils'; +import { getDataViewFieldOrCreateFromColumnMeta } from '@kbn/data-view-utils'; export class FieldRow { readonly name: string; @@ -62,7 +63,11 @@ export class FieldRow { this.name = name; this.flattenedValue = flattenedValue; - this.dataViewField = dataView.getFieldByName(name); + this.dataViewField = getDataViewFieldOrCreateFromColumnMeta({ + dataView, + fieldName: name, + columnMeta: columnsMeta?.[name], + }); this.isPinned = isPinned; this.columnsMeta = columnsMeta; } diff --git a/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json b/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json index cd6d92a846707..793cc22447e76 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json +++ b/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json @@ -38,7 +38,8 @@ "@kbn/core-lifecycle-browser", "@kbn/management-settings-ids", "@kbn/apm-types", - "@kbn/event-stacktrace" + "@kbn/event-stacktrace", + "@kbn/data-view-utils" ], "exclude": [ diff --git a/test/functional/apps/discover/esql/_esql_view.ts b/test/functional/apps/discover/esql/_esql_view.ts index 06531f5f9c141..f4c92caea6caa 100644 --- a/test/functional/apps/discover/esql/_esql_view.ts +++ b/test/functional/apps/discover/esql/_esql_view.ts @@ -474,6 +474,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('sorting', () => { + beforeEach(async () => { + await common.navigateToApp('discover'); + await timePicker.setDefaultAbsoluteRange(); + }); + it('should sort correctly', async () => { const savedSearchName = 'testSorting'; @@ -640,6 +645,100 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Sort fields\n2' ); }); + + it('should sort on custom vars too', async () => { + const savedSearchName = 'testSortingForCustomVars'; + + await discover.selectTextBaseLang(); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + const testQuery = + 'from logstash-* | sort @timestamp | limit 100 | keep bytes | eval var0 = abs(bytes) + 1'; + await monacoEditor.setCodeEditorValue(testQuery); + await testSubjects.click('querySubmitButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + await retry.waitFor('first cell contains an initial value', async () => { + const cell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + const text = await cell.getVisibleText(); + return text === '1,624'; + }); + + expect(await testSubjects.getVisibleText('dataGridColumnSortingButton')).to.be( + 'Sort fields' + ); + + await dataGrid.clickDocSortDesc('var0', 'Sort High-Low'); + + await discover.waitUntilSearchingHasFinished(); + + await retry.waitFor('first cell contains the highest value', async () => { + const cell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + const text = await cell.getVisibleText(); + return text === '17,967'; + }); + + expect(await testSubjects.getVisibleText('dataGridColumnSortingButton')).to.be( + 'Sort fields\n1' + ); + + await discover.saveSearch(savedSearchName); + + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + await retry.waitFor('first cell contains the same highest value', async () => { + const cell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + const text = await cell.getVisibleText(); + return text === '17,967'; + }); + + await browser.refresh(); + + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + await retry.waitFor('first cell contains the same highest value after reload', async () => { + const cell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + const text = await cell.getVisibleText(); + return text === '17,967'; + }); + + await discover.clickNewSearchButton(); + + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + await discover.loadSavedSearch(savedSearchName); + + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + await retry.waitFor( + 'first cell contains the same highest value after reopening', + async () => { + const cell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + const text = await cell.getVisibleText(); + return text === '17,967'; + } + ); + + await dataGrid.clickDocSortDesc('var0', 'Sort Low-High'); + + await discover.waitUntilSearchingHasFinished(); + + await retry.waitFor('first cell contains the lowest value', async () => { + const cell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + const text = await cell.getVisibleText(); + return text === '1'; + }); + + expect(await testSubjects.getVisibleText('dataGridColumnSortingButton')).to.be( + 'Sort fields\n1' + ); + }); }); describe('filtering by clicking on the table', () => {