diff --git a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.test.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.test.ts new file mode 100644 index 000000000000..9a33b0cfa6f1 --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { IndexPattern } from './index_pattern'; + +// @ts-expect-error +import mockLogStashFields from './fixtures/logstash_fields'; +import { stubbedSavedObjectIndexPattern } from './fixtures/stubbed_saved_object_index_pattern'; + +import { fieldFormatsMock } from '../../field_formats/mocks'; +import { flattenHitWrapper } from './flatten_hit'; + +class MockFieldFormatter {} + +fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; + +// helper function to create index patterns +function create(id: string) { + const { + type, + version, + attributes: { timeFieldName, fields, title }, + } = stubbedSavedObjectIndexPattern(id); + + return new IndexPattern({ + spec: { + id, + type, + version, + timeFieldName, + fields, + title, + runtimeFieldMap: {}, + }, + fieldFormats: fieldFormatsMock, + shortDotsEnable: false, + metaFields: [], + }); +} + +describe('flattenHit', () => { + let indexPattern: IndexPattern; + + // create an indexPattern instance for each test + beforeEach(() => { + indexPattern = create('test-pattern'); + }); + + it('returns sorted object keys that combine _source, fields and metaFields in a defined order', () => { + const response = flattenHitWrapper(indexPattern, ['_id', '_type', '_score', '_routing'])({ + _id: 'a', + _source: { + name: 'first', + }, + fields: { + date: ['1'], + zzz: ['z'], + }, + }); + const expectedOrder = ['date', 'name', 'zzz', '_id', '_routing', '_score', '_type']; + expect(Object.keys(response)).toEqual(expectedOrder); + expect(Object.entries(response).map(([key]) => key)).toEqual(expectedOrder); + }); +}); diff --git a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts index dadf302ec6eb..7cd88c8a87c1 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts @@ -75,7 +75,26 @@ function decorateFlattenedWrapper(hit: Record, metaFields: Record { + return Reflect.ownKeys(target).sort((a, b) => { + const aIsMeta = _.includes(metaFields, a); + const bIsMeta = _.includes(metaFields, b); + if (aIsMeta && bIsMeta) { + return String(a).localeCompare(String(b)); + } + if (aIsMeta) { + return 1; + } + if (bIsMeta) { + return -1; + } + return String(a).localeCompare(String(b)); + }); + }, + }); }; } diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts index abbc52946059..050959dff98a 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts @@ -13,10 +13,15 @@ import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks describe('Row formatter', () => { const hit = { - foo: 'bar', - number: 42, - hello: '

World

', - also: 'with "quotes" or \'single quotes\'', + _id: 'a', + _type: 'doc', + _score: 1, + _source: { + foo: 'bar', + number: 42, + hello: '

World

', + also: 'with "quotes" or \'single quotes\'', + }, }; const createIndexPattern = () => { @@ -37,12 +42,17 @@ describe('Row formatter', () => { const indexPattern = createIndexPattern(); + // Realistic response with alphabetical insertion order const formatHitReturnValue = { also: 'with \\"quotes\\" or 'single qoutes'', - number: '42', foo: 'bar', + number: '42', hello: '<h1>World</h1>', + _id: 'a', + _type: 'doc', + _score: 1, }; + const formatHitMock = jest.fn().mockReturnValue(formatHitReturnValue); beforeEach(() => { @@ -52,7 +62,7 @@ describe('Row formatter', () => { it('formats document properly', () => { expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( - `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
number:
42
foo:
bar
hello:
<h1>World</h1>
"` + `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
number:
42
hello:
<h1>World</h1>
_id:
a
_type:
doc
_score:
1
"` ); }); @@ -60,7 +70,7 @@ describe('Row formatter', () => { expect( formatRow({ ...hit, highlight: { number: '42' } }, indexPattern).trim() ).toMatchInlineSnapshot( - `"
number:
42
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
hello:
<h1>World</h1>
"` + `"
number:
42
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
hello:
<h1>World</h1>
_id:
a
_type:
doc
_score:
1
"` ); }); @@ -88,6 +98,21 @@ describe('Row formatter', () => { ); }); + it('formats top level objects in alphabetical order', () => { + indexPattern.getFieldByName = jest.fn().mockReturnValue({ + name: 'subfield', + }); + indexPattern.getFormatterForField = jest.fn().mockReturnValue({ + convert: () => 'formatted', + }); + const formatted = formatTopLevelObject( + { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, + { 'a.zzz': [100], 'a.ccc': [50] }, + indexPattern + ).trim(); + expect(formatted.indexOf('
a.ccc:
')).toBeLessThan(formatted.indexOf('
a.zzz:
')); + }); + it('formats top level objects with subfields and highlights', () => { indexPattern.getFieldByName = jest.fn().mockReturnValue({ name: 'subfield', diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts index e17e840e4048..a226cefb5396 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts @@ -26,6 +26,7 @@ export const doTemplate = template(noWhiteSpace(templateHtml)); export const formatRow = (hit: Record, indexPattern: IndexPattern) => { const highlights = hit?.highlight ?? {}; + // Keys are sorted in the hits object const formatted = indexPattern.formatHit(hit); const highlightPairs: Array<[string, unknown]> = []; const sourcePairs: Array<[string, unknown]> = []; @@ -44,7 +45,8 @@ export const formatTopLevelObject = ( const highlights = row.highlight ?? {}; const highlightPairs: Array<[string, unknown]> = []; const sourcePairs: Array<[string, unknown]> = []; - Object.entries(fields).forEach(([key, values]) => { + const sorted = Object.entries(fields).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); + sorted.forEach(([key, values]) => { const field = indexPattern.getFieldByName(key); const formatter = field ? indexPattern.getFormatterForField(field) diff --git a/test/functional/apps/discover/_large_string.ts b/test/functional/apps/discover/_large_string.ts index 775b92ffc054..5dd5834ffe10 100644 --- a/test/functional/apps/discover/_large_string.ts +++ b/test/functional/apps/discover/_large_string.ts @@ -32,8 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('verify the large string book present', async function () { const ExpectedDoc = - '_id:1 _type:_doc _index:testlargestring _score:0' + - ' mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' + + 'mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' + ' This eBook is for the use of anyone anywhere in the United States' + ' and most other parts of the world at no cost and with almost no restrictions whatsoever.' + ' You may copy it, give it away or re-use it under the terms of the' + @@ -42,7 +41,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ' you’ll have to check the laws of the country where you are' + ' located before using this ebook.' + ' Title: Hamlet Author: William Shakespeare Release Date: November 1998 [EBook #1524]' + - ' Last Updated: December 30, 2017 Language: English Character set encoding:'; + ' Last Updated: December 30, 2017 Language: English Character set encoding:' + + ' _id:1 _type:_doc _index:testlargestring _score:0'; let rowData; await PageObjects.common.navigateToApp('discover'); diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index e023b3194204..5ecfa80c240a 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - '_id:doc1 _type:_doc _index:dlstest _score:0 region.keyword:EAST name:ABC Company name.keyword:ABC Company region:EAST' + 'name:ABC Company name.keyword:ABC Company region:EAST region.keyword:EAST _id:doc1 _index:dlstest _score:0 _type:_doc' ); }); after('logout', async () => { diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 97a701444523..67ac8f98117e 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -113,7 +113,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - '_id:2 _type:_doc _index:flstest _score:0 customer_name.keyword:ABC Company customer_ssn:444.555.6666 customer_region.keyword:WEST runtime_customer_ssn:444.555.6666 calculated at runtime customer_region:WEST customer_name:ABC Company customer_ssn.keyword:444.555.6666' + 'customer_name:ABC Company customer_name.keyword:ABC Company customer_region:WEST customer_region.keyword:WEST customer_ssn:444.555.6666 customer_ssn.keyword:444.555.6666 runtime_customer_ssn:444.555.6666 calculated at runtime _id:2 _index:flstest _score:0 _type:_doc' ); }); @@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - '_id:2 _type:_doc _index:flstest _score:0 customer_name.keyword:ABC Company customer_region.keyword:WEST customer_region:WEST customer_name:ABC Company' + 'customer_name:ABC Company customer_name.keyword:ABC Company customer_region:WEST customer_region.keyword:WEST _id:2 _index:flstest _score:0 _type:_doc' ); });