From b21e1ebf3806793f09770cc03d7a65a83fac5e17 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 14 Oct 2021 14:43:06 +0200 Subject: [PATCH 1/5] Deprecate DataView.flattenHit in favor of data plugin flattenHit (#114517) * WIP replacing indexPattern.flattenHit by tabify * Fix jest tests * Read metaFields from index pattern * Remove old test code * remove unnecessary changes * Remove flattenHitWrapper APIs * Fix imports * Fix missing metaFields * Add all meta fields to allowlist * Improve inline comments * Move flattenHit test to new implementation * Add deprecation comment to implementation Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/tabify_docs.test.ts.snap | 160 +---------------- .../data/common/search/tabify/index.ts | 2 +- .../common/search/tabify/tabify_docs.test.ts | 170 ++++++++++++------ .../data/common/search/tabify/tabify_docs.ts | 85 ++++++++- src/plugins/data/public/index.ts | 2 - .../data_views/common/data_views/data_view.ts | 3 + .../common/data_views/flatten_hit.ts | 17 +- src/plugins/data_views/public/index.ts | 2 +- .../public/__mocks__/index_pattern.ts | 9 +- .../__mocks__/index_pattern_with_timefield.ts | 9 +- .../doc_table/components/table_row.tsx | 3 +- .../sidebar/discover_sidebar.test.tsx | 4 +- .../discover_sidebar_responsive.test.tsx | 6 +- .../sidebar/lib/field_calculator.js | 4 +- .../sidebar/lib/field_calculator.test.ts | 3 +- .../apps/main/utils/calc_field_counts.ts | 4 +- .../discover_grid/discover_grid.tsx | 4 +- .../discover_grid_cell_actions.tsx | 6 +- .../discover_grid_document_selection.test.tsx | 45 ++--- .../discover_grid_expand_button.test.tsx | 36 ++-- .../get_render_cell_value.test.tsx | 33 ++-- .../components/table/table.test.tsx | 6 +- .../application/components/table/table.tsx | 3 +- .../generate_csv/generate_csv.test.ts | 1 + .../generate_csv/generate_csv.ts | 2 +- 25 files changed, 293 insertions(+), 326 deletions(-) diff --git a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap index 22276335a0599d..9a85ba57ce5ef7 100644 --- a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap +++ b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`tabifyDocs combines meta fields if meta option is set 1`] = ` +exports[`tabify_docs tabifyDocs converts fields by default 1`] = ` Object { "columns": Array [ Object { @@ -108,7 +108,7 @@ Object { } `; -exports[`tabifyDocs converts fields by default 1`] = ` +exports[`tabify_docs tabifyDocs converts source if option is set 1`] = ` Object { "columns": Array [ Object { @@ -216,7 +216,7 @@ Object { } `; -exports[`tabifyDocs converts source if option is set 1`] = ` +exports[`tabify_docs tabifyDocs skips nested fields if option is set 1`] = ` Object { "columns": Array [ Object { @@ -324,115 +324,7 @@ Object { } `; -exports[`tabifyDocs skips nested fields if option is set 1`] = ` -Object { - "columns": Array [ - Object { - "id": "fieldTest", - "meta": Object { - "field": "fieldTest", - "index": "test-index", - "params": Object { - "id": "number", - }, - "type": "number", - }, - "name": "fieldTest", - }, - Object { - "id": "invalidMapping", - "meta": Object { - "field": "invalidMapping", - "index": "test-index", - "params": undefined, - "type": "number", - }, - "name": "invalidMapping", - }, - Object { - "id": "nested", - "meta": Object { - "field": "nested", - "index": "test-index", - "params": undefined, - "type": "object", - }, - "name": "nested", - }, - Object { - "id": "sourceTest", - "meta": Object { - "field": "sourceTest", - "index": "test-index", - "params": Object { - "id": "number", - }, - "type": "number", - }, - "name": "sourceTest", - }, - Object { - "id": "_id", - "meta": Object { - "field": "_id", - "index": "test-index", - "params": undefined, - "type": "string", - }, - "name": "_id", - }, - Object { - "id": "_index", - "meta": Object { - "field": "_index", - "index": "test-index", - "params": undefined, - "type": "string", - }, - "name": "_index", - }, - Object { - "id": "_score", - "meta": Object { - "field": "_score", - "index": "test-index", - "params": undefined, - "type": "number", - }, - "name": "_score", - }, - Object { - "id": "_type", - "meta": Object { - "field": "_type", - "index": "test-index", - "params": undefined, - "type": "string", - }, - "name": "_type", - }, - ], - "rows": Array [ - Object { - "_id": "hit-id-value", - "_index": "hit-index-value", - "_score": 77, - "_type": "hit-type-value", - "fieldTest": 123, - "invalidMapping": 345, - "nested": Array [ - Object { - "field": 123, - }, - ], - "sourceTest": 123, - }, - ], - "type": "datatable", -} -`; - -exports[`tabifyDocs works without provided index pattern 1`] = ` +exports[`tabify_docs tabifyDocs works without provided index pattern 1`] = ` Object { "columns": Array [ Object { @@ -475,53 +367,9 @@ Object { }, "name": "sourceTest", }, - Object { - "id": "_id", - "meta": Object { - "field": "_id", - "index": undefined, - "params": undefined, - "type": "string", - }, - "name": "_id", - }, - Object { - "id": "_index", - "meta": Object { - "field": "_index", - "index": undefined, - "params": undefined, - "type": "string", - }, - "name": "_index", - }, - Object { - "id": "_score", - "meta": Object { - "field": "_score", - "index": undefined, - "params": undefined, - "type": "number", - }, - "name": "_score", - }, - Object { - "id": "_type", - "meta": Object { - "field": "_type", - "index": undefined, - "params": undefined, - "type": "string", - }, - "name": "_type", - }, ], "rows": Array [ Object { - "_id": "hit-id-value", - "_index": "hit-index-value", - "_score": 77, - "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ diff --git a/src/plugins/data/common/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts index 74fbc7ba4cfa4a..279ff705f231c8 100644 --- a/src/plugins/data/common/search/tabify/index.ts +++ b/src/plugins/data/common/search/tabify/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export { tabifyDocs } from './tabify_docs'; +export { tabifyDocs, flattenHit } from './tabify_docs'; export { tabifyAggResponse } from './tabify'; export { tabifyGetColumns } from './get_columns'; diff --git a/src/plugins/data/common/search/tabify/tabify_docs.test.ts b/src/plugins/data/common/search/tabify/tabify_docs.test.ts index 1a3f7195b722e1..a2910a1be4a9a4 100644 --- a/src/plugins/data/common/search/tabify/tabify_docs.test.ts +++ b/src/plugins/data/common/search/tabify/tabify_docs.test.ts @@ -6,71 +6,137 @@ * Side Public License, v 1. */ -import { tabifyDocs } from './tabify_docs'; -import { IndexPattern } from '../..'; +import { tabifyDocs, flattenHit } from './tabify_docs'; +import { IndexPattern, DataView } from '../..'; import type { estypes } from '@elastic/elasticsearch'; -describe('tabifyDocs', () => { - const fieldFormats = { - getInstance: (id: string) => ({ toJSON: () => ({ id }) }), - getDefaultInstance: (id: string) => ({ toJSON: () => ({ id }) }), - }; +import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; +import { stubbedSavedObjectIndexPattern } from '../../../../data_views/common/data_view.stub'; - const index = new IndexPattern({ +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 DataView({ spec: { - id: 'test-index', - fields: { - sourceTest: { name: 'sourceTest', type: 'number', searchable: true, aggregatable: true }, - fieldTest: { name: 'fieldTest', type: 'number', searchable: true, aggregatable: true }, - 'nested.field': { - name: 'nested.field', - type: 'number', - searchable: true, - aggregatable: true, - }, - }, + id, + type, + version, + timeFieldName, + fields: JSON.parse(fields), + title, + runtimeFieldMap: {}, }, - fieldFormats: fieldFormats as any, + fieldFormats: fieldFormatsMock, + shortDotsEnable: false, + metaFields: ['_id', '_type', '_score', '_routing'], }); +} - // @ts-expect-error not full inteface - const response = { - hits: { - hits: [ +describe('tabify_docs', () => { + describe('flattenHit', () => { + let indexPattern: DataView; + + // 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 = flattenHit( { - _id: 'hit-id-value', - _index: 'hit-index-value', - _type: 'hit-type-value', - _score: 77, - _source: { sourceTest: 123 }, - fields: { fieldTest: 123, invalidMapping: 345, nested: [{ field: 123 }] }, + _index: 'foobar', + _id: 'a', + _source: { + name: 'first', + }, + fields: { + date: ['1'], + zzz: ['z'], + _abc: ['a'], + }, }, - ], - }, - } as estypes.SearchResponse; - - it('converts fields by default', () => { - const table = tabifyDocs(response, index); - expect(table).toMatchSnapshot(); + indexPattern + ); + const expectedOrder = ['_abc', 'date', 'name', 'zzz', '_id', '_routing', '_score', '_type']; + expect(Object.keys(response)).toEqual(expectedOrder); + expect(Object.entries(response).map(([key]) => key)).toEqual(expectedOrder); + }); }); - it('converts source if option is set', () => { - const table = tabifyDocs(response, index, { source: true }); - expect(table).toMatchSnapshot(); - }); + describe('tabifyDocs', () => { + const fieldFormats = { + getInstance: (id: string) => ({ toJSON: () => ({ id }) }), + getDefaultInstance: (id: string) => ({ toJSON: () => ({ id }) }), + }; - it('skips nested fields if option is set', () => { - const table = tabifyDocs(response, index, { shallow: true }); - expect(table).toMatchSnapshot(); - }); + const index = new IndexPattern({ + spec: { + id: 'test-index', + fields: { + sourceTest: { name: 'sourceTest', type: 'number', searchable: true, aggregatable: true }, + fieldTest: { name: 'fieldTest', type: 'number', searchable: true, aggregatable: true }, + 'nested.field': { + name: 'nested.field', + type: 'number', + searchable: true, + aggregatable: true, + }, + }, + }, + metaFields: ['_id', '_index', '_score', '_type'], + fieldFormats: fieldFormats as any, + }); - it('combines meta fields if meta option is set', () => { - const table = tabifyDocs(response, index, { meta: true }); - expect(table).toMatchSnapshot(); - }); + // @ts-expect-error not full inteface + const response = { + hits: { + hits: [ + { + _id: 'hit-id-value', + _index: 'hit-index-value', + _type: 'hit-type-value', + _score: 77, + _source: { sourceTest: 123 }, + fields: { fieldTest: 123, invalidMapping: 345, nested: [{ field: 123 }] }, + }, + ], + }, + } as estypes.SearchResponse; + + it('converts fields by default', () => { + const table = tabifyDocs(response, index); + expect(table).toMatchSnapshot(); + }); + + it('converts source if option is set', () => { + const table = tabifyDocs(response, index, { source: true }); + expect(table).toMatchSnapshot(); + }); + + it('skips nested fields if option is set', () => { + const table = tabifyDocs(response, index, { shallow: true }); + expect(table).toMatchSnapshot(); + }); + + it('combines meta fields from index pattern', () => { + const table = tabifyDocs(response, index); + expect(table.columns.map((col) => col.id)).toEqual( + expect.arrayContaining(['_id', '_index', '_score', '_type']) + ); + }); - it('works without provided index pattern', () => { - const table = tabifyDocs(response); - expect(table).toMatchSnapshot(); + it('works without provided index pattern', () => { + const table = tabifyDocs(response); + expect(table).toMatchSnapshot(); + }); }); }); diff --git a/src/plugins/data/common/search/tabify/tabify_docs.ts b/src/plugins/data/common/search/tabify/tabify_docs.ts index 8e628e7741df56..4259488771761c 100644 --- a/src/plugins/data/common/search/tabify/tabify_docs.ts +++ b/src/plugins/data/common/search/tabify/tabify_docs.ts @@ -11,12 +11,60 @@ import { isPlainObject } from 'lodash'; import { IndexPattern } from '../..'; import { Datatable, DatatableColumn, DatatableColumnType } from '../../../../expressions/common'; +type ValidMetaFieldNames = keyof Pick< + estypes.SearchHit, + | '_id' + | '_ignored' + | '_index' + | '_node' + | '_primary_term' + | '_routing' + | '_score' + | '_seq_no' + | '_shard' + | '_source' + | '_type' + | '_version' +>; +const VALID_META_FIELD_NAMES: ValidMetaFieldNames[] = [ + '_id', + '_ignored', + '_index', + '_node', + '_primary_term', + '_routing', + '_score', + '_seq_no', + '_shard', + '_source', + '_type', + '_version', +]; + +function isValidMetaFieldName(field: string): field is ValidMetaFieldNames { + // Since the array above is more narrowly typed than string[], we cannot use + // string to find a value in here. We manually cast it to wider string[] type + // so we're able to use `includes` on it. + return (VALID_META_FIELD_NAMES as string[]).includes(field); +} + export interface TabifyDocsOptions { shallow?: boolean; + /** + * If set to `false` the _source of the document, if requested, won't be + * merged into the flattened document. + */ source?: boolean; - meta?: boolean; } +/** + * Flattens an individual hit (from an ES response) into an object. This will + * create flattened field names, like `user.name`. + * + * @param hit The hit from an ES reponse's hits.hits[] + * @param indexPattern The index pattern for the requested index if available. + * @param params Parameters how to flatten the hit + */ export function flattenHit( hit: estypes.SearchHit, indexPattern?: IndexPattern, @@ -62,13 +110,36 @@ export function flattenHit( if (params?.source !== false && hit._source) { flatten(hit._source as Record); } - if (params?.meta !== false) { - // combine the fields that Discover allows to add as columns - const { _id, _index, _type, _score } = hit; - flatten({ _id, _index, _score, _type }); - } - return flat; + // Merge all valid meta fields into the flattened object + // expect for _source (in case that was specified as a meta field) + indexPattern?.metaFields?.forEach((metaFieldName) => { + if (!isValidMetaFieldName(metaFieldName) || metaFieldName === '_source') { + return; + } + flat[metaFieldName] = hit[metaFieldName]; + }); + + // Use a proxy to make sure that keys are always returned in a specific order, + // so we have a guarantee on the flattened order of keys. + return new Proxy(flat, { + ownKeys: (target) => { + return Reflect.ownKeys(target).sort((a, b) => { + const aIsMeta = indexPattern?.metaFields?.includes(String(a)); + const bIsMeta = indexPattern?.metaFields?.includes(String(b)); + if (aIsMeta && bIsMeta) { + return String(a).localeCompare(String(b)); + } + if (aIsMeta) { + return 1; + } + if (bIsMeta) { + return -1; + } + return String(a).localeCompare(String(b)); + }); + }, + }); } export const tabifyDocs = ( diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 3ae98c083976e1..4d51a7ae0ad772 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -52,7 +52,6 @@ import { ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, validateDataView, - flattenHitWrapper, } from './data_views'; export type { IndexPatternsService } from './data_views'; @@ -69,7 +68,6 @@ export const indexPatterns = { getFieldSubtypeMulti, getFieldSubtypeNested, validate: validateDataView, - flattenHitWrapper, }; export { diff --git a/src/plugins/data_views/common/data_views/data_view.ts b/src/plugins/data_views/common/data_views/data_view.ts index 5768ebe635729d..57db127208dc3c 100644 --- a/src/plugins/data_views/common/data_views/data_view.ts +++ b/src/plugins/data_views/common/data_views/data_view.ts @@ -72,6 +72,9 @@ export class DataView implements IIndexPattern { formatField: FormatFieldFn; }; public formatField: FormatFieldFn; + /** + * @deprecated Use `flattenHit` utility method exported from data plugin instead. + */ public flattenHit: (hit: Record, deep?: boolean) => Record; public metaFields: string[]; /** diff --git a/src/plugins/data_views/common/data_views/flatten_hit.ts b/src/plugins/data_views/common/data_views/flatten_hit.ts index ddf484affa2982..0a6388f0914b15 100644 --- a/src/plugins/data_views/common/data_views/flatten_hit.ts +++ b/src/plugins/data_views/common/data_views/flatten_hit.ts @@ -6,6 +6,11 @@ * Side Public License, v 1. */ +// --------- DEPRECATED --------- +// This implementation of flattenHit is deprecated and should no longer be used. +// If you consider adding features to this, please don't but use the `flattenHit` +// implementation from the data plugin. + import _ from 'lodash'; import { DataView } from './data_view'; @@ -114,15 +119,3 @@ export function flattenHitWrapper(dataView: DataView, metaFields = {}, cache = n return decorateFlattened(flattened); }; } - -/** - * This wraps `flattenHitWrapper` so one single cache can be provided for all uses of that - * function. The returned value of this function is what is included in the index patterns - * setup contract. - * - * @public - */ -export function createFlattenHitWrapper() { - const cache = new WeakMap(); - return _.partial(flattenHitWrapper, _, _, cache); -} diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index 572806df11fa3c..5c810ec1fd4c86 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -13,7 +13,7 @@ export { ILLEGAL_CHARACTERS, validateDataView, } from '../common/lib'; -export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './data_views'; +export { formatHitProvider, onRedirectNoIndexPattern } from './data_views'; export { IndexPatternField, IIndexPatternFieldList, TypeMeta } from '../common'; diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index f9cc202f9063e2..2acb512617a6b4 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { IIndexPatternFieldList } from '../../../data/common'; +import type { estypes } from '@elastic/elasticsearch'; +import { flattenHit, IIndexPatternFieldList } from '../../../data/common'; import { IndexPattern } from '../../../data/common'; -import { indexPatterns } from '../../../data/public'; const fields = [ { @@ -85,10 +85,11 @@ const indexPattern = { getFormatterForField: () => ({ convert: () => 'formatted' }), } as unknown as IndexPattern; -indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; indexPattern.formatField = (hit: Record, fieldName: string) => { - return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; + return fieldName === '_source' + ? hit._source + : flattenHit(hit as unknown as estypes.SearchHit, indexPattern)[fieldName]; }; export const indexPatternMock = indexPattern; diff --git a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts index 0f64a6c67741d0..6cf8e8b3485ff9 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { IIndexPatternFieldList } from '../../../data/common'; +import { flattenHit, IIndexPatternFieldList } from '../../../data/common'; import { IndexPattern } from '../../../data/common'; -import { indexPatterns } from '../../../data/public'; +import type { estypes } from '@elastic/elasticsearch'; const fields = [ { @@ -76,10 +76,11 @@ const indexPattern = { popularizeField: () => {}, } as unknown as IndexPattern; -indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; indexPattern.formatField = (hit: Record, fieldName: string) => { - return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; + return fieldName === '_source' + ? hit._source + : flattenHit(hit as unknown as estypes.SearchHit, indexPattern)[fieldName]; }; export const indexPatternWithTimefieldMock = indexPattern; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx index 8d56f2adeaf650..d91735460af085 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx @@ -10,6 +10,7 @@ import React, { Fragment, useCallback, useMemo, useState } from 'react'; import classNames from 'classnames'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiIcon } from '@elastic/eui'; +import { flattenHit } from '../../../../../../../../data/common'; import { DocViewer } from '../../../../../components/doc_viewer/doc_viewer'; import { FilterManager, IndexPattern } from '../../../../../../../../data/public'; import { TableCell } from './table_row/table_cell'; @@ -57,7 +58,7 @@ export const TableRow = ({ }); const anchorDocTableRowSubj = row.isAnchor ? ' docTableAnchorRow' : ''; - const flattenedRow = useMemo(() => indexPattern.flattenHit(row), [indexPattern, row]); + const flattenedRow = useMemo(() => flattenHit(row, indexPattern), [indexPattern, row]); const mapping = useMemo(() => indexPattern.fields.getByName, [indexPattern]); // toggle display of the rows details, a full list of the fields from each row diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx index e53bf006e2b4ed..a550dbd59b9fa7 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx @@ -15,7 +15,7 @@ import realHits from '../../../../../__fixtures__/real_hits.js'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import { DiscoverSidebarProps } from './discover_sidebar'; -import { IndexPatternAttributes } from '../../../../../../../data/common'; +import { flattenHit, IndexPatternAttributes } from '../../../../../../../data/common'; import { SavedObject } from '../../../../../../../../core/types'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebarComponent as DiscoverSidebar } from './discover_sidebar'; @@ -44,7 +44,7 @@ function getCompProps(): DiscoverSidebarProps { const fieldCounts: Record = {}; for (const hit of hits) { - for (const key of Object.keys(indexPattern.flattenHit(hit))) { + for (const key of Object.keys(flattenHit(hit, indexPattern))) { fieldCounts[key] = (fieldCounts[key] || 0) + 1; } } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx index 9d73f885c988da..ded7897d2a9e5a 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -15,7 +15,7 @@ import realHits from '../../../../../__fixtures__/real_hits.js'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { IndexPatternAttributes } from '../../../../../../../data/common'; +import { flattenHit, IndexPatternAttributes } from '../../../../../../../data/common'; import { SavedObject } from '../../../../../../../../core/types'; import { DiscoverSidebarResponsive, @@ -72,7 +72,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps { const indexPattern = stubLogstashIndexPattern; // @ts-expect-error _.each() is passing additional args to flattenHit - const hits = each(cloneDeep(realHits), indexPattern.flattenHit) as Array< + const hits = each(cloneDeep(realHits), (hit) => flattenHit(hit, indexPattern)) as Array< Record > as ElasticSearchHit[]; @@ -83,7 +83,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps { ]; for (const hit of hits) { - for (const key of Object.keys(indexPattern.flattenHit(hit))) { + for (const key of Object.keys(flattenHit(hit, indexPattern))) { mockfieldCounts[key] = (mockfieldCounts[key] || 0) + 1; } } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js index 8f86cdad82cf74..be7e9c616273db 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js @@ -8,12 +8,12 @@ import { map, sortBy, without, each, defaults, isObject } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { flattenHit } from '../../../../../../../../data/common'; function getFieldValues(hits, field, indexPattern) { const name = field.name; - const flattenHit = indexPattern.flattenHit; return map(hits, function (hit) { - return flattenHit(hit)[name]; + return flattenHit(hit, indexPattern)[name]; }); } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts index c3ff7970c5aac9..d4bc41f36b2d41 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts @@ -13,6 +13,7 @@ import { keys, each, cloneDeep, clone, uniq, filter, map } from 'lodash'; import realHits from '../../../../../../__fixtures__/real_hits.js'; import { IndexPattern } from '../../../../../../../../data/public'; +import { flattenHit } from '../../../../../../../../data/common'; // @ts-expect-error import { fieldCalculator } from './field_calculator'; @@ -120,7 +121,7 @@ describe('fieldCalculator', function () { let hits: any; beforeEach(function () { - hits = each(cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)); + hits = each(cloneDeep(realHits), (hit) => flattenHit(hit, indexPattern)); }); it('Should return an array of values for _source fields', function () { diff --git a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts index 1ce7023539be4b..211c4e5c8b0698 100644 --- a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts +++ b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { IndexPattern } from 'src/plugins/data/common'; +import { flattenHit, IndexPattern } from '../../../../../../data/common'; import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; /** @@ -22,7 +22,7 @@ export function calcFieldCounts( return {}; } for (const hit of rows) { - const fields = Object.keys(indexPattern.flattenHit(hit)); + const fields = Object.keys(flattenHit(hit, indexPattern)); for (const fieldName of fields) { counts[fieldName] = (counts[fieldName] || 0) + 1; } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index 0fe506b3b8537e..11323080274a98 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -21,7 +21,7 @@ import { EuiLoadingSpinner, EuiIcon, } from '@elastic/eui'; -import type { IndexPattern } from 'src/plugins/data/common'; +import { flattenHit, IndexPattern } from '../../../../../data/common'; import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; import { getSchemaDetectors } from './discover_grid_schema'; import { DiscoverGridFlyout } from './discover_grid_flyout'; @@ -271,7 +271,7 @@ export const DiscoverGrid = ({ getRenderCellValueFn( indexPattern, displayedRows, - displayedRows ? displayedRows.map((hit) => indexPattern.flattenHit(hit)) : [], + displayedRows ? displayedRows.map((hit) => flattenHit(hit, indexPattern)) : [], useNewFieldsApi, fieldsToShow, services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED) diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx index b1823eb3d668c8..a31b551821ddb3 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx @@ -9,7 +9,7 @@ import React, { useContext } from 'react'; import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPatternField } from '../../../../../data/common'; +import { flattenHit, IndexPatternField } from '../../../../../data/common'; import { DiscoverGridContext } from './discover_grid_context'; export const FilterInBtn = ({ @@ -27,7 +27,7 @@ export const FilterInBtn = ({ { const row = context.rows[rowIndex]; - const flattened = context.indexPattern.flattenHit(row); + const flattened = flattenHit(row, context.indexPattern); if (flattened) { context.onFilter(columnId, flattened[columnId], '+'); @@ -60,7 +60,7 @@ export const FilterOutBtn = ({ { const row = context.rows[rowIndex]; - const flattened = context.indexPattern.flattenHit(row); + const flattened = flattenHit(row, context.indexPattern); if (flattened) { context.onFilter(columnId, flattened[columnId], '-'); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx index 41cf3f5a68edbf..e9b93e21553a2a 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx @@ -17,6 +17,17 @@ import { esHits } from '../../../__mocks__/es_hits'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { DiscoverGridContext } from './discover_grid_context'; +const baseContextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + selectedDocs: [], + setSelectedDocs: jest.fn(), +}; + describe('document selection', () => { describe('getDocId', () => { test('doc with custom routing', () => { @@ -39,14 +50,7 @@ describe('document selection', () => { describe('SelectButton', () => { test('is not checked', () => { const contextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), + ...baseContextMock, }; const component = mountWithIntl( @@ -68,14 +72,8 @@ describe('document selection', () => { test('is checked', () => { const contextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, + ...baseContextMock, selectedDocs: ['i::1::'], - setSelectedDocs: jest.fn(), }; const component = mountWithIntl( @@ -97,14 +95,7 @@ describe('document selection', () => { test('adding a selection', () => { const contextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), + ...baseContextMock, }; const component = mountWithIntl( @@ -126,14 +117,8 @@ describe('document selection', () => { }); test('removing a selection', () => { const contextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, + ...baseContextMock, selectedDocs: ['i::1::'], - setSelectedDocs: jest.fn(), }; const component = mountWithIntl( diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx index d1299b39a25b2e..3f7cb70091cfa6 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.test.tsx @@ -14,17 +14,21 @@ import { DiscoverGridContext } from './discover_grid_context'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { esHits } from '../../../__mocks__/es_hits'; +const baseContextMock = { + expanded: undefined, + setExpanded: jest.fn(), + rows: esHits, + onFilter: jest.fn(), + indexPattern: indexPatternMock, + isDarkMode: false, + selectedDocs: [], + setSelectedDocs: jest.fn(), +}; + describe('Discover grid view button ', function () { it('when no document is expanded, setExpanded is called with current document', async () => { const contextMock = { - expanded: undefined, - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), + ...baseContextMock, }; const component = mountWithIntl( @@ -45,14 +49,8 @@ describe('Discover grid view button ', function () { }); it('when the current document is expanded, setExpanded is called with undefined', async () => { const contextMock = { + ...baseContextMock, expanded: esHits[0], - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), }; const component = mountWithIntl( @@ -73,14 +71,8 @@ describe('Discover grid view button ', function () { }); it('when another document is expanded, setExpanded is called with the current document', async () => { const contextMock = { + ...baseContextMock, expanded: esHits[0], - setExpanded: jest.fn(), - rows: esHits, - onFilter: jest.fn(), - indexPattern: indexPatternMock, - isDarkMode: false, - selectedDocs: [], - setSelectedDocs: jest.fn(), }; const component = mountWithIntl( diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx index 5aca237d465813..6556876217953c 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx @@ -11,6 +11,7 @@ import { ReactWrapper, shallow } from 'enzyme'; import { getRenderCellValueFn } from './get_render_cell_value'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { flattenHit } from 'src/plugins/data/common'; jest.mock('../../../../../kibana_react/public', () => ({ useUiSetting: () => true, @@ -68,12 +69,16 @@ const rowsFieldsWithTopLevelObject: ElasticSearchHit[] = [ }, ]; +const flatten = (hit: ElasticSearchHit): Record => { + return flattenHit(hit, indexPatternMock); +}; + describe('Discover grid cell rendering', function () { it('renders bytes column correctly', () => { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsSource, - rowsSource.map((row) => indexPatternMock.flattenHit(row)), + rowsSource.map(flatten), false, [], 100 @@ -95,7 +100,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsSource, - rowsSource.map((row) => indexPatternMock.flattenHit(row)), + rowsSource.map(flatten), false, [], 100 @@ -146,7 +151,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsSource, - rowsSource.map((row) => indexPatternMock.flattenHit(row)), + rowsSource.map(flatten), false, [], 100 @@ -189,7 +194,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFields, - rowsFields.map((row) => indexPatternMock.flattenHit(row)), + rowsFields.map(flatten), true, [], 100 @@ -244,7 +249,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFields, - rowsFields.map((row) => indexPatternMock.flattenHit(row)), + rowsFields.map(flatten), true, [], // this is the number of rendered items @@ -287,7 +292,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFields, - rowsFields.map((row) => indexPatternMock.flattenHit(row)), + rowsFields.map(flatten), true, [], 100 @@ -335,7 +340,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFieldsWithTopLevelObject, - rowsFieldsWithTopLevelObject.map((row) => indexPatternMock.flattenHit(row)), + rowsFieldsWithTopLevelObject.map(flatten), true, [], 100 @@ -376,7 +381,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFieldsWithTopLevelObject, - rowsFieldsWithTopLevelObject.map((row) => indexPatternMock.flattenHit(row)), + rowsFieldsWithTopLevelObject.map(flatten), true, [], 100 @@ -416,7 +421,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFieldsWithTopLevelObject, - rowsFieldsWithTopLevelObject.map((row) => indexPatternMock.flattenHit(row)), + rowsFieldsWithTopLevelObject.map(flatten), true, [], 100 @@ -447,7 +452,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsFieldsWithTopLevelObject, - rowsFieldsWithTopLevelObject.map((row) => indexPatternMock.flattenHit(row)), + rowsFieldsWithTopLevelObject.map(flatten), true, [], 100 @@ -466,7 +471,9 @@ describe('Discover grid cell rendering', function () { @@ -477,7 +484,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsSource, - rowsSource.map((row) => indexPatternMock.flattenHit(row)), + rowsSource.map(flatten), false, [], 100 @@ -499,7 +506,7 @@ describe('Discover grid cell rendering', function () { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, rowsSource, - rowsSource.map((row) => indexPatternMock.flattenHit(row)), + rowsSource.map(flatten), false, [], 100 diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 3f010d9d07737a..ce914edcec7030 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; import { DocViewerTable, DocViewerTableProps } from './table'; -import { indexPatterns, IndexPattern } from '../../../../../data/public'; +import { IndexPattern } from '../../../../../data/public'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; jest.mock('../../../kibana_services', () => ({ @@ -65,7 +65,7 @@ const indexPattern = { ], }, metaFields: ['_index', '_score'], - flattenHit: undefined, + flattenHit: jest.fn(), formatHit: jest.fn((hit) => hit._source), } as unknown as IndexPattern; @@ -73,8 +73,6 @@ indexPattern.fields.getByName = (name: string) => { return indexPattern.fields.getAll().find((field) => field.name === name); }; -indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); - const mountComponent = (props: DocViewerTableProps) => { return mountWithIntl(); }; diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index eab3ba6e3d29a0..7f597d846f88f0 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiInMemoryTable } from '@elastic/eui'; import { IndexPattern, IndexPatternField } from '../../../../../data/public'; +import { flattenHit } from '../../../../../data/common'; import { SHOW_MULTIFIELDS } from '../../../../common'; import { getServices } from '../../../kibana_services'; import { isNestedFieldParent } from '../../apps/main/utils/nested_fields'; @@ -95,7 +96,7 @@ export const DocViewerTable = ({ return null; } - const flattened = indexPattern?.flattenHit(hit); + const flattened = flattenHit(hit, indexPattern, { source: true }); const fieldsToShow = getFieldsToShow(Object.keys(flattened), indexPattern, showMultiFields); const items: FieldRecord[] = Object.keys(flattened) diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index f393661e4c490d..1902c4ed0272e4 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -75,6 +75,7 @@ const mockSearchSourceGetFieldDefault = jest.fn().mockImplementation((key: strin getByName: jest.fn().mockImplementation(() => []), getByType: jest.fn().mockImplementation(() => []), }, + metaFields: ['_id', '_index', '_type', '_score'], getFormatterForField: jest.fn(), }; } diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index c269677ae930d4..6c2989d54309d8 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -356,7 +356,7 @@ export class CsvGenerator { let table: Datatable | undefined; try { - table = tabifyDocs(results, index, { shallow: true, meta: true }); + table = tabifyDocs(results, index, { shallow: true }); } catch (err) { this.logger.error(err); } From f52c718f112bdba3320ac4591a7d3098082f7014 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 14 Oct 2021 09:07:22 -0400 Subject: [PATCH 2/5] fix cloudwatch category assignmet (#114928) --- src/plugins/home/server/tutorials/cloudwatch_logs/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index dd035a66c5cedb..cf0c27ed9be731 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -53,6 +53,6 @@ export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSc onPrem: onPremInstructions([], context), elasticCloud: cloudInstructions(), onPremElasticCloud: onPremCloudInstructions(), - integrationBrowserCategories: ['security', 'network', 'web'], + integrationBrowserCategories: ['aws', 'cloud', 'datastore', 'security', 'network'], }; } From 4db243703649d955500208a4ca89690b52e5fc23 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 14 Oct 2021 15:28:40 +0200 Subject: [PATCH 3/5] [Lens] Thresholds: when computing default static value take into account all layer metrics (#113647) * :sparkles: compute the default threshold based on data bounds * :bug: Fix multi layer types issue * :white_check_mark: Fix test * :white_check_mark: Fix other test * :bug: Fix computation bug for the initial static value * :white_check_mark: Add new suite of test for static value computation * :bug: Fix extents bug and refactor in a single function + tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/xy_visualization/expression.tsx | 30 +- .../public/xy_visualization/state_helpers.ts | 4 + .../threshold_helpers.test.ts | 569 ++++++++++++++++++ .../xy_visualization/threshold_helpers.tsx | 146 +++-- 4 files changed, 700 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 87462e71f3cf69..7aee537ebbeddc 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -67,6 +67,7 @@ import { getThresholdRequiredPaddings, ThresholdAnnotations, } from './expression_thresholds'; +import { computeOverallDataDomain } from './threshold_helpers'; declare global { interface Window { @@ -250,6 +251,10 @@ export function XYChart({ const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); const darkMode = chartsThemeService.useDarkMode(); const filteredLayers = getFilteredLayers(layers, data); + const layersById = filteredLayers.reduce((memo, layer) => { + memo[layer.layerId] = layer; + return memo; + }, {} as Record); const handleCursorUpdate = useActiveCursor(chartsActiveCursorService, chartRef, { datatables: Object.values(data.tables), @@ -386,7 +391,7 @@ export function XYChart({ const extent = axis.groupId === 'left' ? yLeftExtent : yRightExtent; const hasBarOrArea = Boolean( axis.series.some((series) => { - const seriesType = filteredLayers.find((l) => l.layerId === series.layer)?.seriesType; + const seriesType = layersById[series.layer]?.seriesType; return seriesType?.includes('bar') || seriesType?.includes('area'); }) ); @@ -406,20 +411,15 @@ export function XYChart({ ); if (!fit && axisHasThreshold) { // Remove this once the chart will support automatic annotation fit for other type of charts - for (const series of axis.series) { - const table = data.tables[series.layer]; - for (const row of table.rows) { - for (const column of table.columns) { - if (column.id === series.accessor) { - const value = row[column.id]; - if (typeof value === 'number') { - // keep the 0 in view - max = Math.max(value, max || 0, 0); - min = Math.min(value, min || 0, 0); - } - } - } - } + const { min: computedMin, max: computedMax } = computeOverallDataDomain( + filteredLayers, + axis.series.map(({ accessor }) => accessor), + data.tables + ); + + if (computedMin != null && computedMax != null) { + max = Math.max(computedMax, max || 0); + min = Math.min(computedMin, min || 0); } for (const { layerId, yConfig } of thresholdLayers) { const table = data.tables[layerId]; diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index 4edf7fdf5e512a..14a82011cb5264 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -26,6 +26,10 @@ export function isPercentageSeries(seriesType: SeriesType) { ); } +export function isStackedChart(seriesType: SeriesType) { + return seriesType.includes('stacked'); +} + export function isHorizontalChart(layers: Array<{ seriesType: SeriesType }>) { return layers.every((l) => isHorizontalSeries(l.seriesType)); } diff --git a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts new file mode 100644 index 00000000000000..d7286de0316d67 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts @@ -0,0 +1,569 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { XYLayerConfig } from '../../common/expressions'; +import { FramePublicAPI } from '../types'; +import { computeOverallDataDomain, getStaticValue } from './threshold_helpers'; + +function getActiveData(json: Array<{ id: string; rows: Array> }>) { + return json.reduce((memo, { id, rows }) => { + const columns = Object.keys(rows[0]).map((columnId) => ({ + id: columnId, + name: columnId, + meta: { type: 'number' as const }, + })); + memo[id] = { + type: 'datatable' as const, + columns, + rows, + }; + return memo; + }, {} as NonNullable); +} + +describe('threshold helpers', () => { + describe('getStaticValue', () => { + const hasDateHistogram = () => false; + const hasAllNumberHistogram = () => true; + + it('should return fallback value on missing data', () => { + expect(getStaticValue([], 'x', {}, hasAllNumberHistogram)).toBe(100); + }); + + it('should return fallback value on no-configuration/missing hit on current data', () => { + // no-config: missing layer + expect( + getStaticValue( + [], + 'x', + { + activeData: getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(100); + // accessor id has no hit in data + expect( + getStaticValue( + [{ layerId: 'id-a', seriesType: 'area' } as XYLayerConfig], // missing xAccessor for groupId == x + 'x', + { + activeData: getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(100); + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['d'], + } as XYLayerConfig, + ], // missing hit of accessor "d" in data + 'yLeft', + { + activeData: getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(100); + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a'], + } as XYLayerConfig, + ], // missing yConfig fallbacks to left axis, but the requested group is yRight + 'yRight', + { + activeData: getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(100); + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a'], + } as XYLayerConfig, + ], // same as above with x groupId + 'x', + { + activeData: getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(100); + }); + + it('should work for no yConfig defined and fallback to left axis', () => { + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a'], + } as XYLayerConfig, + ], + 'yLeft', + { + activeData: getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(75); // 3/4 of "a" only + }); + + it('should extract axis side from yConfig', () => { + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a'], + yConfig: [{ forAccessor: 'a', axisMode: 'right' }], + } as XYLayerConfig, + ], + 'yRight', + { + activeData: getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(75); // 3/4 of "a" only + }); + + it('should correctly distribute axis on left and right with different formatters when in auto', () => { + const tables = getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 200, c: 100 }) }, + ]); + tables['id-a'].columns[0].meta.params = { id: 'number' }; // a: number formatter + tables['id-a'].columns[1].meta.params = { id: 'percent' }; // b: percent formatter + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a', 'b'], + } as XYLayerConfig, + ], + 'yLeft', + { activeData: tables }, + hasAllNumberHistogram + ) + ).toBe(75); // 3/4 of "a" only + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a', 'b'], + } as XYLayerConfig, + ], + 'yRight', + { activeData: tables }, + hasAllNumberHistogram + ) + ).toBe(150); // 3/4 of "b" only + }); + + it('should ignore hasHistogram for left or right axis', () => { + const tables = getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 200, c: 100 }) }, + ]); + tables['id-a'].columns[0].meta.params = { id: 'number' }; // a: number formatter + tables['id-a'].columns[1].meta.params = { id: 'percent' }; // b: percent formatter + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a', 'b'], + } as XYLayerConfig, + ], + 'yLeft', + { activeData: tables }, + hasDateHistogram + ) + ).toBe(75); // 3/4 of "a" only + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a', 'b'], + } as XYLayerConfig, + ], + 'yRight', + { activeData: tables }, + hasDateHistogram + ) + ).toBe(150); // 3/4 of "b" only + }); + + it('should early exit for x group if a date histogram is detected', () => { + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + xAccessor: 'a', + accessors: [], + } as XYLayerConfig, + ], + 'x', // this is influenced by the callback + { + activeData: getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + ]), + }, + hasDateHistogram + ) + ).toBe(100); + }); + + it('should not force zero-based interval for x group', () => { + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + xAccessor: 'a', + accessors: [], + } as XYLayerConfig, + ], + 'x', + { + activeData: getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ a: i % 2 ? 33 : 50 })), + }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(45.75); // 33 (min) + (50 - 33) * 3/4 + }); + }); + + describe('computeOverallDataDomain', () => { + it('should compute the correct value for a single layer with stacked series', () => { + for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked']) + expect( + computeOverallDataDomain( + [{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] } as XYLayerConfig], + ['a', 'b', 'c'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ + a: i === 0 ? 25 : null, + b: i === 1 ? 50 : null, + c: i === 2 ? 75 : null, + })), + }, + ]) + ) + ).toEqual({ min: 0, max: 150 }); // there's just one series with 150, so the lowerbound fallbacks to 0 + }); + + it('should work for percentage series', () => { + for (const seriesType of [ + 'bar_percentage_stacked', + 'bar_horizontal_percentage_stacked', + 'area_percentage_stacked', + ]) + expect( + computeOverallDataDomain( + [{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] } as XYLayerConfig], + ['a', 'b', 'c'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ + a: i === 0 ? 0.25 : null, + b: i === 1 ? 0.25 : null, + c: i === 2 ? 0.25 : null, + })), + }, + ]) + ) + ).toEqual({ min: 0, max: 0.75 }); + }); + + it('should compute the correct value for multiple layers with stacked series', () => { + for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked']) { + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] }, + { layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] }, + ] as XYLayerConfig[], + ['a', 'b', 'c', 'd', 'e', 'f'], + getActiveData([ + { id: 'id-a', rows: [{ a: 25, b: 100, c: 100 }] }, + { id: 'id-b', rows: [{ d: 50, e: 50, f: 50 }] }, + ]) + ) + ).toEqual({ min: 0, max: 375 }); + // same as before but spread on 3 rows with nulls + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] }, + { layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] }, + ] as XYLayerConfig[], + ['a', 'b', 'c', 'd', 'e', 'f'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ + a: i === 0 ? 25 : null, + b: i === 1 ? 100 : null, + c: i === 2 ? 100 : null, + })), + }, + { + id: 'id-b', + rows: Array(3) + .fill(1) + .map((_, i) => ({ + d: i === 0 ? 50 : null, + e: i === 1 ? 50 : null, + f: i === 2 ? 50 : null, + })), + }, + ]) + ) + ).toEqual({ min: 0, max: 375 }); + } + }); + + it('should compute the correct value for multiple layers with non-stacked series', () => { + for (const seriesType of ['bar', 'bar_horizontal', 'line', 'area']) + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] }, + { layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] }, + ] as XYLayerConfig[], + ['a', 'b', 'c', 'd', 'e', 'f'], + getActiveData([ + { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, + { id: 'id-b', rows: Array(3).fill({ d: 50, e: 50, f: 50 }) }, + ]) + ) + ).toEqual({ min: 50, max: 100 }); + }); + + it('should compute the correct value for mixed series (stacked + non-stacked)', () => { + for (const nonStackedSeries of ['bar', 'bar_horizontal', 'line', 'area']) { + for (const stackedSeries of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked']) { + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType: nonStackedSeries, accessors: ['a', 'b', 'c'] }, + { layerId: 'id-b', seriesType: stackedSeries, accessors: ['d', 'e', 'f'] }, + ] as XYLayerConfig[], + ['a', 'b', 'c', 'd', 'e', 'f'], + getActiveData([ + { id: 'id-a', rows: [{ a: 100, b: 100, c: 100 }] }, + { id: 'id-b', rows: [{ d: 50, e: 50, f: 50 }] }, + ]) + ) + ).toEqual({ + min: 0, // min is 0 as there is at least one stacked series + max: 150, // max is id-b layer accessor sum + }); + } + } + }); + + it('should compute the correct value for a histogram stacked chart', () => { + for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked']) + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType, xAccessor: 'c', accessors: ['a', 'b'] }, + { layerId: 'id-b', seriesType, xAccessor: 'f', accessors: ['d', 'e'] }, + ] as XYLayerConfig[], + ['a', 'b', 'd', 'e'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ a: 50 * i, b: 100 * i, c: i })), + }, + { + id: 'id-b', + rows: Array(3) + .fill(1) + .map((_, i) => ({ d: 25 * (i + 1), e: i % 2 ? 100 : null, f: i })), + }, + ]) + ) + ).toEqual({ min: 0, max: 375 }); + }); + + it('should compute the correct value for a histogram non-stacked chart', () => { + for (const seriesType of ['bar', 'bar_horizontal', 'line', 'area']) + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType, xAccessor: 'c', accessors: ['a', 'b'] }, + { layerId: 'id-b', seriesType, xAccessor: 'f', accessors: ['d', 'e'] }, + ] as XYLayerConfig[], + ['a', 'b', 'd', 'e'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ a: 50 * i, b: 100 * i, c: i })), + }, + { + id: 'id-b', + rows: Array(3) + .fill(1) + .map((_, i) => ({ d: 25 * (i + 1), e: i % 2 ? 100 : null, f: i })), + }, + ]) + ) + ).toEqual({ min: 0, max: 200 }); + }); + + it('should compute the result taking into consideration negative-based intervals too', () => { + // stacked + expect( + computeOverallDataDomain( + [ + { + layerId: 'id-a', + seriesType: 'area_stacked', + accessors: ['a', 'b', 'c'], + } as XYLayerConfig, + ], + ['a', 'b', 'c'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ + a: i === 0 ? -100 : null, + b: i === 1 ? 200 : null, + c: i === 2 ? 100 : null, + })), + }, + ]) + ) + ).toEqual({ min: 0, max: 200 }); // it is stacked, so max is the sum and 0 is the fallback + expect( + computeOverallDataDomain( + [{ layerId: 'id-a', seriesType: 'area', accessors: ['a', 'b', 'c'] } as XYLayerConfig], + ['a', 'b', 'c'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ + a: i === 0 ? -100 : null, + b: i === 1 ? 200 : null, + c: i === 2 ? 100 : null, + })), + }, + ]) + ) + ).toEqual({ min: -100, max: 200 }); + }); + + it('should return no result if no layers or accessors are passed', () => { + expect( + computeOverallDataDomain( + [], + ['a', 'b', 'c'], + getActiveData([{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }]) + ) + ).toEqual({ min: undefined, max: undefined }); + }); + + it('should return no result if data or table is not available', () => { + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType: 'area', accessors: ['a', 'b', 'c'] }, + { layerId: 'id-b', seriesType: 'line', accessors: ['d', 'e', 'f'] }, + ] as XYLayerConfig[], + ['a', 'b'], + getActiveData([{ id: 'id-c', rows: [{ a: 100, b: 100 }] }]) // mind the layer id here + ) + ).toEqual({ min: undefined, max: undefined }); + + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType: 'bar', accessors: ['a', 'b', 'c'] }, + { layerId: 'id-b', seriesType: 'bar_stacked' }, + ] as XYLayerConfig[], + ['a', 'b'], + getActiveData([]) + ) + ).toEqual({ min: undefined, max: undefined }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx index ec473507094731..8bf5f84b15bad6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import { partition } from 'lodash'; import { layerTypes } from '../../common'; import type { XYLayerConfig, YConfig } from '../../common/expressions'; import { Datatable } from '../../../../../src/plugins/expressions/public'; import type { DatasourcePublicAPI, FramePublicAPI } from '../types'; import { groupAxesByType } from './axes_configuration'; -import { isPercentageSeries } from './state_helpers'; +import { isPercentageSeries, isStackedChart } from './state_helpers'; import type { XYState } from './types'; import { checkScaleOperation } from './visualization_helpers'; @@ -91,14 +92,22 @@ export function getStaticValue( // filter and organize data dimensions into threshold groups // now pick the columnId in the active data - const { dataLayer, accessor } = getAccessorCriteriaForGroup(groupId, dataLayers, activeData); - if (groupId === 'x' && dataLayer && !layerHasNumberHistogram(dataLayer)) { + const { + dataLayers: filteredLayers, + untouchedDataLayers, + accessors, + } = getAccessorCriteriaForGroup(groupId, dataLayers, activeData); + if ( + groupId === 'x' && + filteredLayers.length && + !untouchedDataLayers.some(layerHasNumberHistogram) + ) { return fallbackValue; } return ( computeStaticValueForGroup( - dataLayer, - accessor, + filteredLayers, + accessors, activeData, groupId !== 'x' // histogram axis should compute the min based on the current data ) || fallbackValue @@ -111,54 +120,123 @@ function getAccessorCriteriaForGroup( activeData: FramePublicAPI['activeData'] ) { switch (groupId) { - case 'x': - const dataLayer = dataLayers.find(({ xAccessor }) => xAccessor); + case 'x': { + const filteredDataLayers = dataLayers.filter(({ xAccessor }) => xAccessor); + // need to reshape the dataLayers to match the other accessors format return { - dataLayer, - accessor: dataLayer?.xAccessor, + dataLayers: filteredDataLayers.map(({ accessors, xAccessor, ...rest }) => ({ + ...rest, + accessors: [xAccessor] as string[], + })), + // need the untouched ones for some checks later on + untouchedDataLayers: filteredDataLayers, + accessors: filteredDataLayers.map(({ xAccessor }) => xAccessor) as string[], }; - case 'yLeft': + } + case 'yLeft': { const { left } = groupAxesByType(dataLayers, activeData); + const leftIds = new Set(left.map(({ layer }) => layer)); + const filteredDataLayers = dataLayers.filter(({ layerId }) => leftIds.has(layerId)); return { - dataLayer: dataLayers.find(({ layerId }) => layerId === left[0]?.layer), - accessor: left[0]?.accessor, + dataLayers: filteredDataLayers, + untouchedDataLayers: filteredDataLayers, + accessors: left.map(({ accessor }) => accessor), }; - case 'yRight': + } + case 'yRight': { const { right } = groupAxesByType(dataLayers, activeData); + const rightIds = new Set(right.map(({ layer }) => layer)); + const filteredDataLayers = dataLayers.filter(({ layerId }) => rightIds.has(layerId)); return { - dataLayer: dataLayers.find(({ layerId }) => layerId === right[0]?.layer), - accessor: right[0]?.accessor, + dataLayers: filteredDataLayers, + untouchedDataLayers: filteredDataLayers, + accessors: right.map(({ accessor }) => accessor), }; + } + } +} + +export function computeOverallDataDomain( + dataLayers: Array>, + accessorIds: string[], + activeData: NonNullable +) { + const accessorMap = new Set(accessorIds); + let min: number | undefined; + let max: number | undefined; + const [stacked, unstacked] = partition(dataLayers, ({ seriesType }) => + isStackedChart(seriesType) + ); + for (const { layerId, accessors } of unstacked) { + const table = activeData[layerId]; + if (table) { + for (const accessor of accessors) { + if (accessorMap.has(accessor)) { + for (const row of table.rows) { + const value = row[accessor]; + if (typeof value === 'number') { + // when not stacked, do not keep the 0 + max = max != null ? Math.max(value, max) : value; + min = min != null ? Math.min(value, min) : value; + } + } + } + } + } + } + // stacked can span multiple layers, so compute an overall max/min by bucket + const stackedResults: Record = {}; + for (const { layerId, accessors, xAccessor } of stacked) { + const table = activeData[layerId]; + if (table) { + for (const accessor of accessors) { + if (accessorMap.has(accessor)) { + for (const row of table.rows) { + const value = row[accessor]; + // start with a shared bucket + let bucket = 'shared'; + // but if there's an xAccessor use it as new bucket system + if (xAccessor) { + bucket = row[xAccessor]; + } + if (typeof value === 'number') { + stackedResults[bucket] = stackedResults[bucket] ?? 0; + stackedResults[bucket] += value; + } + } + } + } + } + } + + for (const value of Object.values(stackedResults)) { + // for stacked extents keep 0 in view + max = Math.max(value, max || 0, 0); + min = Math.min(value, min || 0, 0); } + + return { min, max }; } function computeStaticValueForGroup( - dataLayer: XYLayerConfig | undefined, - accessorId: string | undefined, + dataLayers: Array>, + accessorIds: string[], activeData: NonNullable, - minZeroBased: boolean + minZeroOrNegativeBase: boolean = true ) { const defaultThresholdFactor = 3 / 4; - if (dataLayer && accessorId) { - if (isPercentageSeries(dataLayer?.seriesType)) { + if (dataLayers.length && accessorIds.length) { + if (dataLayers.some(({ seriesType }) => isPercentageSeries(seriesType))) { return defaultThresholdFactor; } - const tableId = Object.keys(activeData).find((key) => - activeData[key].columns.some(({ id }) => id === accessorId) - ); - if (tableId) { - const columnMax = activeData[tableId].rows.reduce( - (max, row) => Math.max(row[accessorId], max), - -Infinity - ); - const columnMin = activeData[tableId].rows.reduce( - (max, row) => Math.min(row[accessorId], max), - Infinity - ); + + const { min, max } = computeOverallDataDomain(dataLayers, accessorIds, activeData); + + if (min != null && max != null && isFinite(min) && isFinite(max)) { // Custom axis bounds can go below 0, so consider also lower values than 0 - const finalMinValue = minZeroBased ? Math.min(0, columnMin) : columnMin; - const interval = columnMax - finalMinValue; + const finalMinValue = minZeroOrNegativeBase ? Math.min(0, min) : min; + const interval = max - finalMinValue; return Number((finalMinValue + interval * defaultThresholdFactor).toFixed(2)); } } From 586682a0c480a37e04aa717aceb2dcfa39835495 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 14 Oct 2021 15:49:35 +0200 Subject: [PATCH 4/5] [Discover] Rename default column in the advanced settings (#114100) * [Discover] Rename default column in the advanced settings * Fix eslint * Rename default column to an empty string * Fix typo * Fix default column filtering * Update comment * Make an empty array a default columns * Improve functional test * Wording change Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/main/utils/get_state_defaults.ts | 11 +++++++-- .../application/helpers/state_helpers.ts | 7 +++++- src/plugins/discover/server/ui_settings.ts | 5 ++-- .../apps/discover/_discover_fields_api.ts | 24 ++++++++++++++++++- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index 11ebf0ecf9af48..f2f6e4a002aaf3 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -6,9 +6,13 @@ * Side Public License, v 1. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, isEqual } from 'lodash'; import { IUiSettingsClient } from 'kibana/public'; -import { DEFAULT_COLUMNS_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; +import { + DEFAULT_COLUMNS_SETTING, + SEARCH_FIELDS_FROM_SOURCE, + SORT_DEFAULT_ORDER_SETTING, +} from '../../../../../common'; import { SavedSearch } from '../../../../saved_searches'; import { DataPublicPluginStart } from '../../../../../../data/public'; @@ -19,6 +23,9 @@ function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) if (savedSearch.columns && savedSearch.columns.length > 0) { return [...savedSearch.columns]; } + if (config.get(SEARCH_FIELDS_FROM_SOURCE) && isEqual(config.get(DEFAULT_COLUMNS_SETTING), [])) { + return ['_source']; + } return [...config.get(DEFAULT_COLUMNS_SETTING)]; } diff --git a/src/plugins/discover/public/application/helpers/state_helpers.ts b/src/plugins/discover/public/application/helpers/state_helpers.ts index fd17ec9516ab58..bb64f823d61a62 100644 --- a/src/plugins/discover/public/application/helpers/state_helpers.ts +++ b/src/plugins/discover/public/application/helpers/state_helpers.ts @@ -24,6 +24,7 @@ export function handleSourceColumnState( } const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const defaultColumns = uiSettings.get(DEFAULT_COLUMNS_SETTING); + if (useNewFieldsApi) { // if fields API is used, filter out the source column let cleanedColumns = state.columns.filter((column) => column !== '_source'); @@ -39,9 +40,13 @@ export function handleSourceColumnState( } else if (state.columns.length === 0) { // if _source fetching is used and there are no column, switch back to default columns // this can happen if the fields API was previously used + const columns = defaultColumns; + if (columns.length === 0) { + columns.push('_source'); + } return { ...state, - columns: [...defaultColumns], + columns: [...columns], }; } diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index aa1b44da12bfc9..82221fb8e8593b 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -33,9 +33,10 @@ export const getUiSettings: () => Record = () => ({ name: i18n.translate('discover.advancedSettings.defaultColumnsTitle', { defaultMessage: 'Default columns', }), - value: ['_source'], + value: [], description: i18n.translate('discover.advancedSettings.defaultColumnsText', { - defaultMessage: 'Columns displayed by default in the Discovery tab', + defaultMessage: + 'Columns displayed by default in the Discover app. If empty, a summary of the document will be displayed.', }), category: ['discover'], schema: schema.arrayOf(schema.string()), diff --git a/test/functional/apps/discover/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts index 42e2a94b364620..700c865031cd63 100644 --- a/test/functional/apps/discover/_discover_fields_api.ts +++ b/test/functional/apps/discover/_discover_fields_api.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'settings']); const defaultSettings = { defaultIndex: 'logstash-*', 'discover:searchFieldsFromSource': false, @@ -67,5 +67,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.clickDocViewerTab(1); await PageObjects.discover.expectSourceViewerToExist(); }); + + it('switches to _source column when fields API is no longer used', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.toggleAdvancedSettingCheckbox('discover:searchFieldsFromSource'); + + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + expect(await PageObjects.discover.getDocHeader()).to.have.string('_source'); + }); + + it('switches to Document column when fields API is used', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.toggleAdvancedSettingCheckbox('discover:searchFieldsFromSource'); + + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); + }); }); } From cdce98c8a363520e99f59f3f10c59a32586480a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Thu, 14 Oct 2021 15:57:27 +0200 Subject: [PATCH 5/5] [Stack monitoring] Fix clusters functional tests when react is enabled (#114982) * Fix test subjects for overview page * fix pathname matching --- .../public/application/pages/cluster/overview_page.tsx | 2 +- x-pack/plugins/monitoring/public/application/route_init.tsx | 2 +- x-pack/plugins/monitoring/public/directives/main/index.html | 2 +- x-pack/test/functional/services/monitoring/cluster_overview.js | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx index b78df27cd12c41..04074762c8d226 100644 --- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -50,7 +50,7 @@ export const ClusterOverview: React.FC<{}> = () => { { id: 'clusterName', label: clusters[0].cluster_name, - testSubj: 'clusterName', + testSubj: 'overviewTabsclusterName', route: '/overview', }, ]; diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx index c620229eb059a0..52780aa2807076 100644 --- a/x-pack/plugins/monitoring/public/application/route_init.tsx +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -57,7 +57,7 @@ export const RouteInit: React.FC = ({ // check if we need to redirect because of attempt at unsupported multi-cluster monitoring const clusterSupported = cluster.isSupported || clusters.length === 1; - if (location.pathname !== 'home' && !clusterSupported) { + if (location.pathname !== '/home' && !clusterSupported) { return ; } } diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html index c989c71d8c1d44..fd14120e1db2fc 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.html +++ b/x-pack/plugins/monitoring/public/directives/main/index.html @@ -299,7 +299,7 @@

{{pageTitle || monitoringMain.instance}}