diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 9fd43be8dc5b37..9085ae07bbe3ea 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -44,7 +44,6 @@ function create(id: string) { return new IndexPattern({ spec: { id, type, version, timeFieldName, fields, title }, - savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], @@ -214,7 +213,6 @@ describe('IndexPattern', () => { const spec = indexPattern.toSpec(); const restoredPattern = new IndexPattern({ spec, - savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, shortDotsEnable: false, metaFields: [], diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index d38df68e9f4286..a0f27078543a9c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,7 +18,6 @@ */ import _, { each, reject } from 'lodash'; -import { SavedObjectsClientCommon } from '../..'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; @@ -31,10 +30,9 @@ import { SerializedFieldFormat } from '../../../../expressions/common'; interface IndexPatternDeps { spec?: IndexPatternSpec; - savedObjectsClient: SavedObjectsClientCommon; fieldFormats: FieldFormatsStartCommon; - shortDotsEnable: boolean; - metaFields: string[]; + shortDotsEnable?: boolean; + metaFields?: string[]; } interface SavedObjectBody { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index bfd0dc9d946c24..fd3d7a1d138fda 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -435,7 +435,6 @@ export class IndexPatternsService { const indexPattern = new IndexPattern({ spec, - savedObjectsClient: this.savedObjectsClient, fieldFormats: this.fieldFormats, shortDotsEnable, metaFields, 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 new file mode 100644 index 00000000000000..d5ddaa31b8ac3b --- /dev/null +++ b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tabifyDocs converts fields by default 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.field", + "meta": Object { + "field": "nested.field", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "nested.field", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested.field": 123, + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs converts source if option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "sourceTest", + "meta": Object { + "field": "sourceTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "sourceTest", + }, + ], + "rows": Array [ + Object { + "sourceTest": 123, + }, + ], + "type": "datatable", +} +`; + +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", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested": Array [ + Object { + "field": 123, + }, + ], + }, + ], + "type": "datatable", +} +`; + +exports[`tabifyDocs works without provided index pattern 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested.field", + "meta": Object { + "field": "nested.field", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "nested.field", + }, + ], + "rows": Array [ + Object { + "fieldTest": 123, + "invalidMapping": 345, + "nested.field": 123, + }, + ], + "type": "datatable", +} +`; diff --git a/src/plugins/data/common/search/tabify/index.ts b/src/plugins/data/common/search/tabify/index.ts index 90ac3f2fb730b9..9e6657f5e8d83e 100644 --- a/src/plugins/data/common/search/tabify/index.ts +++ b/src/plugins/data/common/search/tabify/index.ts @@ -17,6 +17,26 @@ * under the License. */ +import { SearchResponse } from 'elasticsearch'; +import { SearchSource } from '../search_source'; +import { tabifyAggResponse } from './tabify'; +import { tabifyDocs, TabifyDocsOptions } from './tabify_docs'; +import { TabbedResponseWriterOptions } from './types'; + +export const tabify = ( + searchSource: SearchSource, + esResponse: SearchResponse, + opts: Partial | TabifyDocsOptions +) => { + return !esResponse.aggregations + ? tabifyDocs(esResponse, searchSource.getField('index'), opts as TabifyDocsOptions) + : tabifyAggResponse( + searchSource.getField('aggs'), + esResponse, + opts as Partial + ); +}; + 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 new file mode 100644 index 00000000000000..a1218928561c66 --- /dev/null +++ b/src/plugins/data/common/search/tabify/tabify_docs.test.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { tabifyDocs } from './tabify_docs'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { SearchResponse } from 'elasticsearch'; + +describe('tabifyDocs', () => { + const fieldFormats = { + getInstance: (id: string) => ({ toJSON: () => ({ id }) }), + getDefaultInstance: (id: string) => ({ toJSON: () => ({ id }) }), + }; + + 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, + }, + }, + }, + fieldFormats: fieldFormats as any, + }); + + const response = { + hits: { + hits: [ + { + _source: { sourceTest: 123 }, + fields: { fieldTest: 123, invalidMapping: 345, nested: [{ field: 123 }] }, + }, + ], + }, + } as 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('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 new file mode 100644 index 00000000000000..78ebee9e657172 --- /dev/null +++ b/src/plugins/data/common/search/tabify/tabify_docs.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { isPlainObject } from 'lodash'; +import { IndexPattern } from '../../index_patterns/index_patterns'; +import { Datatable, DatatableColumn, DatatableColumnType } from '../../../../expressions/common'; + +export function flattenHit( + hit: Record, + indexPattern?: IndexPattern, + shallow: boolean = false +) { + const flat = {} as Record; + + function flatten(obj: Record, keyPrefix: string = '') { + for (const [k, val] of Object.entries(obj)) { + const key = keyPrefix + k; + + const field = indexPattern?.fields.getByName(key); + + if (!shallow) { + const isNestedField = field?.type === 'nested'; + if (Array.isArray(val) && !isNestedField) { + val.forEach((v) => isPlainObject(v) && flatten(v, key + '.')); + continue; + } + } else if (flat[key] !== undefined) { + continue; + } + + const hasValidMapping = field?.type !== 'conflict'; + const isValue = !isPlainObject(val); + + if (hasValidMapping || isValue) { + if (!flat[key]) { + flat[key] = val; + } else if (Array.isArray(flat[key])) { + flat[key].push(val); + } else { + flat[key] = [flat[key], val]; + } + continue; + } + + flatten(val, key + '.'); + } + } + + flatten(hit); + return flat; +} + +export interface TabifyDocsOptions { + shallow?: boolean; + source?: boolean; +} + +export const tabifyDocs = ( + esResponse: SearchResponse, + index?: IndexPattern, + params: TabifyDocsOptions = {} +): Datatable => { + const columns: DatatableColumn[] = []; + + const rows = esResponse.hits.hits + .map((hit) => { + const toConvert = params.source ? hit._source : hit.fields; + const flat = flattenHit(toConvert, index, params.shallow); + for (const [key, value] of Object.entries(flat)) { + const field = index?.fields.getByName(key); + const fieldName = field?.name || key; + if (!columns.find((c) => c.id === fieldName)) { + const fieldType = (field?.type as DatatableColumnType) || typeof value; + const formatter = field && index?.getFormatterForField(field); + columns.push({ + id: fieldName, + name: fieldName, + meta: { + type: fieldType, + field: fieldName, + index: index?.id, + params: formatter ? formatter.toJSON() : undefined, + }, + }); + } + } + return flat; + }) + .filter((hit) => hit); + + return { + type: 'datatable', + columns, + rows, + }; +}; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 1390b28ec830db..d2439e3f1573c9 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2283,7 +2283,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 65313adfc0e0f2..3143d5baa5b774 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1113,8 +1113,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:58:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:56:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts