From 48b0866e0c91634d15251fed871997988f98e16e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 14 Aug 2019 16:35:15 +0200 Subject: [PATCH] [ML] Data frame transforms: Fix getting nested properties. (#43262) - Fixes a regression where values were not properly extracted from nested objects. - Moves inline code we had to solve this to a utility function getNestedProperty(). Kibana's idx is a lodash-get replacement with TypeScript support. However, it requires that you'd know the accessor up front, it doesn't work with dynamic string values. getNestedProperty() allows you to pass a string like lodash-get, but it uses idx on the inside so you still get TypeScript support. --- .../__snapshots__/expanded_row.test.tsx.snap | 30 +++++------ .../expanded_row.test.tsx | 34 +++++++++---- .../source_index_preview/expanded_row.tsx | 23 +++------ .../use_source_index_data.ts | 8 +-- .../ml/public/util/object_utils.test.ts | 51 +++++++++++++++++++ .../plugins/ml/public/util/object_utils.ts | 17 +++++++ 6 files changed, 120 insertions(+), 43 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/util/object_utils.test.ts create mode 100644 x-pack/legacy/plugins/ml/public/util/object_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap index 1aeee77882c41a6..41316437e867315 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap @@ -3,67 +3,67 @@ exports[`Data Frame: Test against strings, objects and arrays. 1`] = ` - arrayObject + name : - [{"object1":"the-object-1"},{"object2":"the-objects-2"}] + the-name    - arrayString + nested.inner1 : - ["the-array-string-1","the-array-string-2"] + the-inner-1    - name + nested.inner2 : - the-name + the-inner-2    - nested.inner1 + arrayString : - the-inner-1 + ["the-array-string-1","the-array-string-2"]    - nested.inner2 + arrayObject : - the-inner-2 + [{"object1":"the-object-1"},{"object2":"the-objects-2"}]    diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.test.tsx index b3380df0a7cb9f0..611ae1b892dcc54 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.test.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.test.tsx @@ -7,22 +7,38 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { getNestedProperty } from '../../../../../util/object_utils'; +import { getFlattenedFields } from '../../../../common'; + import { ExpandedRow } from './expanded_row'; describe('Data Frame: ', () => { test('Test against strings, objects and arrays.', () => { + const source = { + name: 'the-name', + nested: { + inner1: 'the-inner-1', + inner2: 'the-inner-2', + }, + arrayString: ['the-array-string-1', 'the-array-string-2'], + arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], + } as Record; + + const flattenedSource = getFlattenedFields(source).reduce( + (p, c) => { + p[c] = getNestedProperty(source, c); + if (p[c] === undefined) { + p[c] = source[`"${c}"`]; + } + return p; + }, + {} as Record + ); + const props = { item: { _id: 'the-id', - _source: { - name: 'the-name', - nested: { - inner1: 'the-inner-1', - inner2: 'the-inner-2', - }, - arrayString: ['the-array-string-1', 'the-array-string-2'], - arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], - }, + _source: flattenedSource, }, }; diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.tsx index f765059a9d95f8b..c69d05134865482 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/expanded_row.tsx @@ -7,25 +7,16 @@ import React from 'react'; import { EuiBadge, EuiText } from '@elastic/eui'; -import { idx } from '@kbn/elastic-idx'; -import { getSelectableFields, EsDoc } from '../../../../common'; +import { EsDoc } from '../../../../common'; -interface ExpandedRowProps { - item: EsDoc; -} - -export const ExpandedRow: React.SFC = ({ item }) => { - const keys = getSelectableFields([item]); - const list = keys.map(k => { - // split the attribute key string and use reduce with an idx check to access nested attributes. - const value = k.split('.').reduce((obj, i) => idx(obj, _ => _[i]), item._source) || ''; - return ( +export const ExpandedRow: React.SFC<{ item: EsDoc }> = ({ item }) => ( + + {Object.entries(item._source).map(([k, value]) => ( {k}: {typeof value === 'string' ? value : JSON.stringify(value)}   - ); - }); - return {list}; -}; + ))} + +); diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/use_source_index_data.ts b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/use_source_index_data.ts index bc7d0ada99b19b9..f873d28371eaa7d 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/use_source_index_data.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/source_index_preview/use_source_index_data.ts @@ -8,11 +8,10 @@ import React, { useEffect, useState } from 'react'; import { SearchResponse } from 'elasticsearch'; -import { idx } from '@kbn/elastic-idx'; - import { StaticIndexPattern } from 'ui/index_patterns'; import { ml } from '../../../../../services/ml_api_service'; +import { getNestedProperty } from '../../../../../util/object_utils'; import { getDefaultSelectableFields, @@ -82,8 +81,11 @@ export const useSourceIndexData = ( [key: string]: any; }; flattenedFields.forEach(ff => { - item[ff] = idx(doc._source, _ => _[ff]); + item[ff] = getNestedProperty(doc._source, ff); if (item[ff] === undefined) { + // If the attribute is undefined, it means it was not a nested property + // but had dots in its actual name. This selects the property by its + // full name and assigns it to `item[ff]`. item[ff] = doc._source[`"${ff}"`]; } }); diff --git a/x-pack/legacy/plugins/ml/public/util/object_utils.test.ts b/x-pack/legacy/plugins/ml/public/util/object_utils.test.ts new file mode 100644 index 000000000000000..ffd0adcbe542da2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/object_utils.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getNestedProperty } from './object_utils'; + +describe('object_utils', () => { + test('getNestedProperty()', () => { + const testObj = { + the: { + nested: { + value: 'the-nested-value', + }, + }, + }; + + const test1 = getNestedProperty(testObj, 'the'); + expect(typeof test1).toBe('object'); + expect(Object.keys(test1)).toStrictEqual(['nested']); + + const test2 = getNestedProperty(testObj, 'the$'); + expect(typeof test2).toBe('undefined'); + + const test3 = getNestedProperty(testObj, 'the$', 'the-default-value'); + expect(typeof test3).toBe('string'); + expect(test3).toBe('the-default-value'); + + const test4 = getNestedProperty(testObj, 'the.neSted'); + expect(typeof test4).toBe('undefined'); + + const test5 = getNestedProperty(testObj, 'the.nested'); + expect(typeof test5).toBe('object'); + expect(Object.keys(test5)).toStrictEqual(['value']); + + const test6 = getNestedProperty(testObj, 'the.nested.vaLue'); + expect(typeof test6).toBe('undefined'); + + const test7 = getNestedProperty(testObj, 'the.nested.value'); + expect(typeof test7).toBe('string'); + expect(test7).toBe('the-nested-value'); + + const test8 = getNestedProperty(testObj, 'the.nested.value.doesntExist'); + expect(typeof test8).toBe('undefined'); + + const test9 = getNestedProperty(testObj, 'the.nested.value.doesntExist', 'the-default-value'); + expect(typeof test9).toBe('string'); + expect(test9).toBe('the-default-value'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/util/object_utils.ts b/x-pack/legacy/plugins/ml/public/util/object_utils.ts new file mode 100644 index 000000000000000..1facc761d6379e3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/object_utils.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { idx } from '@kbn/elastic-idx'; + +// This is similar to lodash's get() except that it's TypeScript aware and is able to infer return types. +// It splits the attribute key string and uses reduce with an idx check to access nested attributes. +export const getNestedProperty = ( + obj: Record, + accessor: string, + defaultValue?: any +) => { + return accessor.split('.').reduce((o, i) => idx(o, _ => _[i]), obj) || defaultValue; +};