* [Index patterns] Guarantee order of fields in flattenHits (#93344) * [Discover] Guarantee order of fields in table preview * Remove comments * Fix test that relied on discover ordering * Fix ordering of test # Conflicts: # test/functional/apps/discover/_large_string.ts # x-pack/test/functional/apps/security/doc_level_security_roles.js # x-pack/test/functional/apps/security/field_level_security.js * Update tests * Fix whitespace
This commit is contained in:
parent
767859cb0c
commit
ab13e4c9fa
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -75,7 +75,26 @@ function decorateFlattenedWrapper(hit: Record<string, any>, metaFields: Record<s
|
|||
}
|
||||
});
|
||||
|
||||
return flattened;
|
||||
// Force all usage of Object.keys to use a predefined sort order,
|
||||
// instead of using insertion order
|
||||
return new Proxy(flattened, {
|
||||
ownKeys: (target) => {
|
||||
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));
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -13,10 +13,15 @@ import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks
|
|||
|
||||
describe('Row formatter', () => {
|
||||
const hit = {
|
||||
foo: 'bar',
|
||||
number: 42,
|
||||
hello: '<h1>World</h1>',
|
||||
also: 'with "quotes" or \'single quotes\'',
|
||||
_id: 'a',
|
||||
_type: 'doc',
|
||||
_score: 1,
|
||||
_source: {
|
||||
foo: 'bar',
|
||||
number: 42,
|
||||
hello: '<h1>World</h1>',
|
||||
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(
|
||||
`"<dl class=\\"source truncate-by-height\\"><dt>also:</dt><dd>with \\\\"quotes\\\\" or 'single qoutes'</dd> <dt>number:</dt><dd>42</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd><h1>World</h1></dd> </dl>"`
|
||||
`"<dl class=\\"source truncate-by-height\\"><dt>also:</dt><dd>with \\\\"quotes\\\\" or 'single qoutes'</dd> <dt>foo:</dt><dd>bar</dd> <dt>number:</dt><dd>42</dd> <dt>hello:</dt><dd><h1>World</h1></dd> <dt>_id:</dt><dd>a</dd> <dt>_type:</dt><dd>doc</dd> <dt>_score:</dt><dd>1</dd> </dl>"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -60,7 +70,7 @@ describe('Row formatter', () => {
|
|||
expect(
|
||||
formatRow({ ...hit, highlight: { number: '42' } }, indexPattern).trim()
|
||||
).toMatchInlineSnapshot(
|
||||
`"<dl class=\\"source truncate-by-height\\"><dt>number:</dt><dd>42</dd> <dt>also:</dt><dd>with \\\\"quotes\\\\" or 'single qoutes'</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd><h1>World</h1></dd> </dl>"`
|
||||
`"<dl class=\\"source truncate-by-height\\"><dt>number:</dt><dd>42</dd> <dt>also:</dt><dd>with \\\\"quotes\\\\" or 'single qoutes'</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd><h1>World</h1></dd> <dt>_id:</dt><dd>a</dd> <dt>_type:</dt><dd>doc</dd> <dt>_score:</dt><dd>1</dd> </dl>"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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('<dt>a.ccc:</dt>')).toBeLessThan(formatted.indexOf('<dt>a.zzz:</dt>'));
|
||||
});
|
||||
|
||||
it('formats top level objects with subfields and highlights', () => {
|
||||
indexPattern.getFieldByName = jest.fn().mockReturnValue({
|
||||
name: 'subfield',
|
||||
|
|
|
@ -26,6 +26,7 @@ export const doTemplate = template(noWhiteSpace(templateHtml));
|
|||
|
||||
export const formatRow = (hit: Record<string, any>, 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)
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue