From 995146517c0776872cd784ac56ad1d45ca055774 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 10 Nov 2020 14:31:04 +0100 Subject: [PATCH] [Lens] Performance refactoring for indexpattern fast lookup and Operation support matrix computation (#82829) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../datapanel.test.tsx | 254 +++++++------- .../indexpattern_datasource/datapanel.tsx | 5 +- .../bucket_nesting_editor.test.tsx | 22 +- .../dimension_panel/bucket_nesting_editor.tsx | 15 +- .../dimension_panel/dimension_editor.tsx | 45 ++- .../dimension_panel/dimension_panel.test.tsx | 74 ++-- .../dimension_panel/droppable.test.ts | 91 +++-- .../dimension_panel/droppable.ts | 6 +- .../dimension_panel/field_select.tsx | 25 +- .../dimension_panel/operation_support.ts | 38 +- .../indexpattern.test.ts | 205 +++++------ .../indexpattern_suggestions.test.tsx | 326 ++++++++++-------- .../indexpattern_suggestions.ts | 32 +- .../layerpanel.test.tsx | 232 +++++++------ .../indexpattern_datasource/loader.test.ts | 13 +- .../public/indexpattern_datasource/loader.ts | 2 + .../public/indexpattern_datasource/mocks.ts | 117 ++++--- .../operations/definitions/cardinality.tsx | 2 +- .../definitions/date_histogram.test.tsx | 79 ++++- .../operations/definitions/date_histogram.tsx | 19 +- .../operations/definitions/metrics.tsx | 2 +- .../definitions/ranges/ranges.test.tsx | 4 + .../operations/definitions/ranges/ranges.tsx | 6 +- .../operations/definitions/terms/index.tsx | 2 +- .../definitions/terms/terms.test.tsx | 2 +- .../operations/operations.test.ts | 50 +-- .../indexpattern_datasource/pure_helpers.ts | 8 +- .../state_helpers.test.ts | 101 +++--- .../public/indexpattern_datasource/types.ts | 1 + .../public/indexpattern_datasource/utils.ts | 10 +- 30 files changed, 996 insertions(+), 792 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index c48bc3dc52404..d2ec1c81bbeec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -18,6 +18,131 @@ import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui'; import { documentField } from './document_field'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { getFieldByNameFactory } from './pure_helpers'; + +const fieldsOne = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'amemory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'client', + displayName: 'client', + type: 'ip', + aggregatable: true, + searchable: true, + }, + documentField, +]; + +const fieldsTwo = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + histogram: { + agg: 'histogram', + interval: 1000, + }, + max: { + agg: 'max', + }, + min: { + agg: 'min', + }, + sum: { + agg: 'sum', + }, + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }, + documentField, +]; + +const fieldsThree = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + documentField, +]; const initialState: IndexPatternPrivateState = { indexPatternRefs: [], @@ -85,139 +210,24 @@ const initialState: IndexPatternPrivateState = { title: 'idx1', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - displayName: 'amemory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'unsupported', - displayName: 'unsupported', - type: 'geo', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'client', - displayName: 'client', - type: 'ip', - aggregatable: true, - searchable: true, - }, - documentField, - ], + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), }, '2': { id: '2', title: 'idx2', timeFieldName: 'timestamp', hasRestrictions: true, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - histogram: { - agg: 'histogram', - interval: 1000, - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - documentField, - ], + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), }, '3': { id: '3', title: 'idx3', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - documentField, - ], + fields: fieldsThree, + getFieldByName: getFieldByNameFactory(fieldsThree), }, }, isFirstExistenceFetch: false, @@ -330,6 +340,7 @@ describe('IndexPattern Data Panel', () => { title: 'aaa', timeFieldName: 'atime', fields: [], + getFieldByName: getFieldByNameFactory([]), hasRestrictions: false, }, b: { @@ -337,6 +348,7 @@ describe('IndexPattern Data Panel', () => { title: 'bbb', timeFieldName: 'btime', fields: [], + getFieldByName: getFieldByNameFactory([]), hasRestrictions: false, }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 28c5605f3bfc5..f2c7d7fc20926 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -5,7 +5,7 @@ */ import './datapanel.scss'; -import { uniq, keyBy, groupBy } from 'lodash'; +import { uniq, groupBy } from 'lodash'; import React, { useState, memo, useCallback, useMemo } from 'react'; import { EuiFlexGroup, @@ -266,9 +266,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions; const unfilteredFieldGroups: FieldGroups = useMemo(() => { - const fieldByName = keyBy(allFields, 'name'); const containsData = (field: IndexPatternField) => { - const overallField = fieldByName[field.name]; + const overallField = currentIndexPattern.getFieldByName(field.name); return ( overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx index 3696f3ad7b102..ee6a86072236c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx @@ -10,12 +10,14 @@ import { BucketNestingEditor } from './bucket_nesting_editor'; import { IndexPatternColumn } from '../indexpattern'; import { IndexPatternField } from '../types'; -const fieldMap = { +const fieldMap: Record = { a: { displayName: 'a' } as IndexPatternField, b: { displayName: 'b' } as IndexPatternField, c: { displayName: 'c' } as IndexPatternField, }; +const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap[name]; + describe('BucketNestingEditor', () => { function mockCol(col: Partial = {}): IndexPatternColumn { const result = { @@ -39,7 +41,7 @@ describe('BucketNestingEditor', () => { it('should display the top level grouping when at the root', () => { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const component = mount( { const setColumns = jest.fn(); const component = mount( , column: IndexPatternColumn) { - return hasField(column) ? fieldMap[column.sourceField]?.displayName || column.sourceField : ''; +function getFieldName( + column: IndexPatternColumn, + getFieldByName: (name: string) => IndexPatternField | undefined +) { + return hasField(column) + ? getFieldByName(column.sourceField)?.displayName || column.sourceField + : ''; } export function BucketNestingEditor({ columnId, layer, setColumns, - fieldMap, + getFieldByName, }: { columnId: string; layer: IndexPatternLayer; setColumns: (columns: string[]) => void; - fieldMap: Record; + getFieldByName: (name: string) => IndexPatternField | undefined; }) { const column = layer.columns[columnId]; const columns = Object.entries(layer.columns); @@ -42,7 +47,7 @@ export function BucketNestingEditor({ .map(([value, c]) => ({ value, text: c.label, - fieldName: getFieldName(fieldMap, c), + fieldName: getFieldName(c, getFieldByName), operationType: c.operationType, })); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index a18cb69db74cb..7cbfbc1749382 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -29,7 +29,7 @@ import { deleteColumn, changeColumn, updateColumnParam, mergeLayer } from '../st import { FieldSelect } from './field_select'; import { hasField, fieldIsInvalid } from '../utils'; import { BucketNestingEditor } from './bucket_nesting_editor'; -import { IndexPattern, IndexPatternField } from '../types'; +import { IndexPattern } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { FormatSelector } from './format_selector'; @@ -97,23 +97,13 @@ export function DimensionEditor(props: DimensionEditorProps) { const ParamEditor = selectedOperationDefinition?.paramEditor; - const fieldMap: Record = useMemo(() => { - const fields: Record = {}; - currentIndexPattern.fields.forEach((field) => { - fields[field.name] = field; - }); - return fields; - }, [currentIndexPattern]); - const possibleOperations = useMemo(() => { return Object.values(operationDefinitionMap) .sort((op1, op2) => { return op1.displayName.localeCompare(op2.displayName); }) .map((def) => def.type) - .filter( - (type) => fieldByOperation[type]?.length || operationWithoutField.indexOf(type) !== -1 - ); + .filter((type) => fieldByOperation[type]?.size || operationWithoutField.has(type)); }, [fieldByOperation, operationWithoutField]); // Operations are compatible if they match inputs. They are always compatible in @@ -128,7 +118,7 @@ export function DimensionEditor(props: DimensionEditorProps) { (selectedColumn && hasField(selectedColumn) && definition.input === 'field' && - fieldByOperation[operationType]?.indexOf(selectedColumn.sourceField) !== -1) || + fieldByOperation[operationType]?.has(selectedColumn.sourceField)) || (selectedColumn && !hasField(selectedColumn) && definition.input !== 'field'), }; }); @@ -198,9 +188,9 @@ export function DimensionEditor(props: DimensionEditorProps) { trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } else if (!selectedColumn || !compatibleWithCurrentField) { - const possibleFields = fieldByOperation[operationType] || []; + const possibleFields = fieldByOperation[operationType] || new Set(); - if (possibleFields.length === 1) { + if (possibleFields.size === 1) { setState( changeColumn({ state, @@ -212,7 +202,7 @@ export function DimensionEditor(props: DimensionEditorProps) { layerId: props.layerId, op: operationType, indexPattern: currentIndexPattern, - field: fieldMap[possibleFields[0]], + field: currentIndexPattern.getFieldByName(possibleFields.values().next().value), previousColumn: selectedColumn, }), }) @@ -236,7 +226,9 @@ export function DimensionEditor(props: DimensionEditorProps) { layerId: props.layerId, op: operationType, indexPattern: currentIndexPattern, - field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined, + field: hasField(selectedColumn) + ? currentIndexPattern.getFieldByName(selectedColumn.sourceField) + : undefined, previousColumn: selectedColumn, }); @@ -297,7 +289,6 @@ export function DimensionEditor(props: DimensionEditorProps) { fieldIsInvalid={currentFieldIsInvalid} currentIndexPattern={currentIndexPattern} existingFields={state.existingFields} - fieldMap={fieldMap} operationSupportMatrix={operationSupportMatrix} selectedColumnOperationType={selectedColumn && selectedColumn.operationType} selectedColumnSourceField={ @@ -323,25 +314,29 @@ export function DimensionEditor(props: DimensionEditorProps) { ) { // If we just changed the field are not in an error state and the operation didn't change, // we use the operations onFieldChange method to calculate the new column. - column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); + column = changeField( + selectedColumn, + currentIndexPattern, + currentIndexPattern.getFieldByName(choice.field)! + ); } else { // Otherwise we'll use the buildColumn method to calculate a new column const compatibleOperations = ('field' in choice && operationSupportMatrix.operationByField[choice.field]) || - []; + new Set(); let operation; - if (compatibleOperations.length > 0) { + if (compatibleOperations.size > 0) { operation = incompatibleSelectedOperationType && - compatibleOperations.includes(incompatibleSelectedOperationType) + compatibleOperations.has(incompatibleSelectedOperationType) ? incompatibleSelectedOperationType - : compatibleOperations[0]; + : compatibleOperations.values().next().value; } else if ('field' in choice) { operation = choice.operationType; } column = buildColumn({ columns: props.state.layers[props.layerId].columns, - field: fieldMap[choice.field], + field: currentIndexPattern.getFieldByName(choice.field), indexPattern: currentIndexPattern, layerId: props.layerId, suggestedPriority: props.suggestedPriority, @@ -417,12 +412,12 @@ export function DimensionEditor(props: DimensionEditorProps) { {!incompatibleSelectedOperationType && !hideGrouping && ( setState(mergeLayer({ state, layerId, newLayer: { columnOrder } })) } + getFieldByName={currentIndexPattern.getFieldByName} /> )} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 92a4dad14dd25..3ed04b08df58f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -22,6 +22,7 @@ import { IndexPatternColumn } from '../operations'; import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; +import { getFieldByNameFactory } from '../pure_helpers'; jest.mock('../loader'); jest.mock('../state_helpers'); @@ -34,6 +35,42 @@ jest.mock('lodash', () => { }; }); +const fields = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + documentField, +]; + const expectedIndexPatterns = { 1: { id: '1', @@ -41,41 +78,8 @@ const expectedIndexPatterns = { timeFieldName: 'timestamp', hasExistence: true, hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - documentField, - ], + fields, + getFieldByName: getFieldByNameFactory(fields), }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index dd696f8be357f..1d85c1f8f78ca 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -15,9 +15,46 @@ import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { IndexPatternColumn } from '../operations'; +import { getFieldByNameFactory } from '../pure_helpers'; jest.mock('../state_helpers'); +const fields = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + documentField, +]; + const expectedIndexPatterns = { 1: { id: '1', @@ -25,41 +62,8 @@ const expectedIndexPatterns = { timeFieldName: 'timestamp', hasExistence: true, hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - documentField, - ], + fields, + getFieldByName: getFieldByNameFactory(fields), }, }; @@ -177,6 +181,23 @@ describe('IndexPatternDimensionEditorPanel', () => { type: 'string', }, ], + + getFieldByName: getFieldByNameFactory([ + { + aggregatable: true, + name: 'bar', + displayName: 'bar', + searchable: true, + type: 'number', + }, + { + aggregatable: true, + name: 'mystring', + displayName: 'mystring', + searchable: true, + type: 'string', + }, + ]), }, }, currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index 7f509cd0244f0..a6ff550af96e9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -137,9 +137,9 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps { currentIndexPattern: IndexPattern; - fieldMap: Record; incompatibleSelectedOperationType: OperationType | null; selectedColumnOperationType?: OperationType; selectedColumnSourceField?: string; @@ -46,7 +45,6 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> { export function FieldSelect({ currentIndexPattern, - fieldMap, incompatibleSelectedOperationType, selectedColumnOperationType, selectedColumnSourceField, @@ -63,31 +61,32 @@ export function FieldSelect({ function isCompatibleWithCurrentOperation(fieldName: string) { if (incompatibleSelectedOperationType) { - return operationByField[fieldName]!.includes(incompatibleSelectedOperationType); + return operationByField[fieldName]!.has(incompatibleSelectedOperationType); } return ( !selectedColumnOperationType || - operationByField[fieldName]!.includes(selectedColumnOperationType) + operationByField[fieldName]!.has(selectedColumnOperationType) ); } const [specialFields, normalFields] = _.partition( fields, - (field) => fieldMap[field].type === 'document' + (field) => currentIndexPattern.getFieldByName(field)?.type === 'document' ); const containsData = (field: string) => - fieldMap[field].type === 'document' || + currentIndexPattern.getFieldByName(field)?.type === 'document' || fieldExists(existingFields, currentIndexPattern.title, field); function fieldNamesToOptions(items: string[]) { return items + .filter((field) => currentIndexPattern.getFieldByName(field)?.displayName) .map((field) => ({ - label: fieldMap[field].displayName, + label: currentIndexPattern.getFieldByName(field)?.displayName, value: { type: 'field', field, - dataType: fieldMap[field].type, + dataType: currentIndexPattern.getFieldByName(field)?.type, operationType: selectedColumnOperationType && isCompatibleWithCurrentOperation(field) ? selectedColumnOperationType @@ -118,7 +117,10 @@ export function FieldSelect({ })); } - const [metaFields, nonMetaFields] = _.partition(normalFields, (field) => fieldMap[field].meta); + const [metaFields, nonMetaFields] = _.partition( + normalFields, + (field) => currentIndexPattern.getFieldByName(field)?.meta + ); const [availableFields, emptyFields] = _.partition(nonMetaFields, containsData); const constructFieldsOptions = (fieldsArr: string[], label: string) => @@ -158,7 +160,6 @@ export function FieldSelect({ incompatibleSelectedOperationType, selectedColumnOperationType, currentIndexPattern, - fieldMap, operationByField, existingFields, ]); @@ -180,7 +181,7 @@ export function FieldSelect({ { label: fieldIsInvalid ? selectedColumnSourceField - : fieldMap[selectedColumnSourceField]?.displayName, + : currentIndexPattern.getFieldByName(selectedColumnSourceField)?.displayName, value: { type: 'field', field: selectedColumnSourceField }, }, ] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 2ea28da201556..31fb5277d53ec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -11,9 +11,9 @@ import { getAvailableOperationsByMetadata } from '../operations'; import { IndexPatternPrivateState } from '../types'; export interface OperationSupportMatrix { - operationByField: Partial>; - operationWithoutField: OperationType[]; - fieldByOperation: Partial>; + operationByField: Partial>>; + operationWithoutField: Set; + fieldByOperation: Partial>>; } type Props = Pick< @@ -31,30 +31,30 @@ export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix currentIndexPattern ).filter((operation) => props.filterOperations(operation.operationMetaData)); - const supportedOperationsByField: Partial> = {}; - const supportedOperationsWithoutField: OperationType[] = []; - const supportedFieldsByOperation: Partial> = {}; + const supportedOperationsByField: Partial>> = {}; + const supportedOperationsWithoutField: Set = new Set(); + const supportedFieldsByOperation: Partial>> = {}; filteredOperationsByMetadata.forEach(({ operations }) => { operations.forEach((operation) => { if (operation.type === 'field') { - supportedOperationsByField[operation.field] = [ - ...(supportedOperationsByField[operation.field] ?? []), - operation.operationType, - ]; - - supportedFieldsByOperation[operation.operationType] = [ - ...(supportedFieldsByOperation[operation.operationType] ?? []), - operation.field, - ]; + if (!supportedOperationsByField[operation.field]) { + supportedOperationsByField[operation.field] = new Set(); + } + supportedOperationsByField[operation.field]?.add(operation.operationType); + + if (!supportedFieldsByOperation[operation.operationType]) { + supportedFieldsByOperation[operation.operationType] = new Set(); + } + supportedFieldsByOperation[operation.operationType]?.add(operation.field); } else if (operation.type === 'none') { - supportedOperationsWithoutField.push(operation.operationType); + supportedOperationsWithoutField.add(operation.operationType); } }); }); return { - operationByField: _.mapValues(supportedOperationsByField, _.uniq), - operationWithoutField: _.uniq(supportedOperationsWithoutField), - fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq), + operationByField: supportedOperationsByField, + operationWithoutField: supportedOperationsWithoutField, + fieldByOperation: supportedFieldsByOperation, }; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index a3f48b162475a..51d95245adb25 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -12,121 +12,128 @@ import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { getFieldByNameFactory } from './pure_helpers'; jest.mock('./loader'); jest.mock('../id_generator'); -const expectedIndexPatterns = { - 1: { - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, +const fieldsOne = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'start_date', + displayName: 'start_date', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + +const fieldsTwo = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', }, - { - name: 'start_date', - displayName: 'start_date', - type: 'date', - aggregatable: true, - searchable: true, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + // Ignored in the UI + histogram: { + agg: 'histogram', + interval: 1000, }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, + avg: { + agg: 'avg', }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, + max: { + agg: 'max', }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, + min: { + agg: 'min', }, - { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, + sum: { + agg: 'sum', }, - ], + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }, +]; + +const expectedIndexPatterns = { + 1: { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), }, 2: { id: '2', title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', hasRestrictions: true, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - // Ignored in the UI - histogram: { - agg: 'histogram', - interval: 1000, - }, - avg: { - agg: 'avg', - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - ], + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index c8cb9fcb33ba9..523a1be34ba3d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -12,121 +12,128 @@ import { getDatasourceSuggestionsFromCurrentState, getDatasourceSuggestionsForVisualizeField, } from './indexpattern_suggestions'; +import { getFieldByNameFactory } from './pure_helpers'; jest.mock('./loader'); jest.mock('../id_generator'); -const expectedIndexPatterns = { - 1: { - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, +const fieldsOne = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'start_date', + displayName: 'start_date', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + +const fieldsTwo = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', }, - { - name: 'start_date', - displayName: 'start_date', - type: 'date', - aggregatable: true, - searchable: true, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + // Ignored in the UI + histogram: { + agg: 'histogram', + interval: 1000, }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, + avg: { + agg: 'avg', }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, + max: { + agg: 'max', }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, + min: { + agg: 'min', }, - { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, + sum: { + agg: 'sum', + }, + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', }, - ], + }, + }, +]; + +const expectedIndexPatterns = { + 1: { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), }, 2: { id: '2', title: 'my-fake-restricted-pattern', hasRestrictions: true, timeFieldName: 'timestamp', - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - // Ignored in the UI - histogram: { - agg: 'histogram', - interval: 1000, - }, - avg: { - agg: 'avg', - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - ], + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), }, }; @@ -335,6 +342,15 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), }, }, layers: { @@ -546,6 +562,16 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }, ], + + getFieldByName: getFieldByNameFactory([ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), }, }, layers: { @@ -1531,6 +1557,43 @@ describe('IndexPattern Data Source suggestions', () => { it('returns simplified versions of table with more than 2 columns', () => { const initialState = testInitialState(); + const fields = [ + { + name: 'field1', + displayName: 'field1', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + displayName: 'field2', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field3', + displayName: 'field3Label', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field4', + displayName: 'field4', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field5', + displayName: 'field5', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]; const state: IndexPatternPrivateState = { indexPatternRefs: [], existingFields: {}, @@ -1540,43 +1603,8 @@ describe('IndexPattern Data Source suggestions', () => { id: '1', title: 'my-fake-index-pattern', hasRestrictions: false, - fields: [ - { - name: 'field1', - displayName: 'field1', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'field2', - displayName: 'field2', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'field3', - displayName: 'field3Label', - type: 'string', - aggregatable: true, - searchable: true, - }, - { - name: 'field4', - displayName: 'field4', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'field5', - displayName: 'field5', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], + fields, + getFieldByName: getFieldByNameFactory(fields), }, }, isFirstExistenceFetch: false, @@ -1700,6 +1728,23 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }, ], + + getFieldByName: getFieldByNameFactory([ + { + name: 'field1', + displayName: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + displayName: 'field2', + type: 'date', + aggregatable: true, + searchable: true, + }, + ]), }, }, isFirstExistenceFetch: false, @@ -1756,6 +1801,15 @@ describe('IndexPattern Data Source suggestions', () => { searchable: true, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'field1', + displayName: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), }, }, isFirstExistenceFetch: false, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 098569d1f460a..c12d7d4be226b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -128,7 +128,7 @@ export function getDatasourceSuggestionsForVisualizeField( const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); // Identify the field by the indexPatternId and the fieldName const indexPattern = state.indexPatterns[indexPatternId]; - const field = indexPattern.fields.find((fld) => fld.name === fieldName); + const field = indexPattern.getFieldByName(fieldName); if (layerIds.length !== 0 || !field) return []; const newId = generateId(); @@ -371,7 +371,7 @@ function createNewLayerWithMetricAggregation( indexPattern: IndexPattern, field: IndexPatternField ): IndexPatternLayer { - const dateField = indexPattern.fields.find((f) => f.name === indexPattern.timeFieldName)!; + const dateField = indexPattern.getFieldByName(indexPattern.timeFieldName!); const column = getMetricColumn(indexPattern, layerId, field); @@ -451,9 +451,8 @@ export function getDatasourceSuggestionsFromCurrentState( (columnId) => layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date' ); - const timeField = indexPattern.fields.find( - ({ name }) => name === indexPattern.timeFieldName - ); + const timeField = + indexPattern.timeFieldName && indexPattern.getFieldByName(indexPattern.timeFieldName); const hasNumericDimension = buckets.length === 1 && @@ -507,17 +506,17 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId const layer = state.layers[layerId]; const [firstBucket, secondBucket, ...rest] = layer.columnOrder; const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] }; - const currentFields = state.indexPatterns[state.currentIndexPatternId].fields; + const indexPattern = state.indexPatterns[state.currentIndexPatternId]; + const firstBucketColumn = layer.columns[firstBucket]; const firstBucketLabel = - currentFields.find((field) => { - const column = layer.columns[firstBucket]; - return hasField(column) && column.sourceField === field.name; - })?.displayName || ''; + (hasField(firstBucketColumn) && + indexPattern.getFieldByName(firstBucketColumn.sourceField)?.displayName) || + ''; + const secondBucketColumn = layer.columns[secondBucket]; const secondBucketLabel = - currentFields.find((field) => { - const column = layer.columns[secondBucket]; - return hasField(column) && column.sourceField === field.name; - })?.displayName || ''; + (hasField(secondBucketColumn) && + indexPattern.getFieldByName(secondBucketColumn.sourceField)?.displayName) || + ''; return buildSuggestion({ state, @@ -604,7 +603,10 @@ function createAlternativeMetricSuggestions( if (!hasField(column)) { return; } - const field = indexPattern.fields.find(({ name }) => column.sourceField === name)!; + const field = indexPattern.getFieldByName(column.sourceField); + if (!field) { + return; + } const alternativeMetricOperations = getOperationTypesForField(field) .map((op) => buildColumn({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 92e35b257f24a..40eb52fe67c6d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -11,6 +11,7 @@ import { shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; import { ShallowWrapper } from 'enzyme'; import { EuiSelectable } from '@elastic/eui'; import { ChangeIndexPattern } from './change_indexpattern'; +import { getFieldByNameFactory } from './pure_helpers'; jest.mock('./state_helpers'); @@ -19,6 +20,120 @@ interface IndexPatternPickerOption { checked?: 'on' | 'off'; } +const fieldsOne = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + +const fieldsTwo = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + histogram: { + agg: 'histogram', + interval: 1000, + }, + max: { + agg: 'max', + }, + min: { + agg: 'min', + }, + sum: { + agg: 'sum', + }, + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }, +]; + +const fieldsThree = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + const initialState: IndexPatternPrivateState = { indexPatternRefs: [ { id: '1', title: 'my-fake-index-pattern' }, @@ -63,129 +178,24 @@ const initialState: IndexPatternPrivateState = { title: 'my-fake-index-pattern', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'unsupported', - displayName: 'unsupported', - type: 'geo', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - ], + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), }, '2': { id: '2', title: 'my-fake-restricted-pattern', hasRestrictions: true, timeFieldName: 'timestamp', - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - histogram: { - agg: 'histogram', - interval: 1000, - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - ], + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), }, '3': { id: '3', title: 'my-compatible-pattern', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - ], + fields: fieldsThree, + getFieldByName: getFieldByNameFactory(fieldsThree), }, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 4222c02388433..adb86253ab28c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -285,15 +285,10 @@ describe('loader', () => { } as unknown) as Pick, }); - expect( - cache.foo.fields.find((f: IndexPatternField) => f.name === 'bytes')!.aggregationRestrictions - ).toEqual({ + expect(cache.foo.getFieldByName('bytes')!.aggregationRestrictions).toEqual({ sum: { agg: 'sum' }, }); - expect( - cache.foo.fields.find((f: IndexPatternField) => f.name === 'timestamp')! - .aggregationRestrictions - ).toEqual({ + expect(cache.foo.getFieldByName('timestamp')!.aggregationRestrictions).toEqual({ date_histogram: { agg: 'date_histogram', fixed_interval: 'm' }, }); }); @@ -342,9 +337,7 @@ describe('loader', () => { } as unknown) as Pick, }); - expect(cache.foo.fields.find((f: IndexPatternField) => f.name === 'timestamp')!.meta).toEqual( - true - ); + expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 70079cce6cc46..fac5d7350e45e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -26,6 +26,7 @@ import { import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public'; import { documentField } from './document_field'; import { readFromStorage, writeToStorage } from '../settings_storage'; +import { getFieldByNameFactory } from './pure_helpers'; type SetState = StateSetter; type SavedObjectsClient = Pick; @@ -112,6 +113,7 @@ export async function loadIndexPatterns({ ]) ), fields: newFields, + getFieldByName: getFieldByNameFactory(newFields), hasRestrictions: !!typeMeta?.aggs, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 744a9f6743d09..2c6f42668d863 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -5,14 +5,11 @@ */ import { DragContextState } from '../drag_drop'; +import { getFieldByNameFactory } from './pure_helpers'; import { IndexPattern } from './types'; -export const createMockedIndexPattern = (): IndexPattern => ({ - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: [ +export const createMockedIndexPattern = (): IndexPattern => { + const fields = [ { name: 'timestamp', displayName: 'timestampLabel', @@ -74,16 +71,19 @@ export const createMockedIndexPattern = (): IndexPattern => ({ lang: 'painless', script: '1234', }, - ], -}); + ]; + return { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields, + getFieldByName: getFieldByNameFactory(fields), + }; +}; -export const createMockedRestrictedIndexPattern = () => ({ - id: '2', - title: 'my-fake-restricted-pattern', - timeFieldName: 'timestamp', - hasRestrictions: true, - fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } }, - fields: [ +export const createMockedRestrictedIndexPattern = () => { + const fields = [ { name: 'timestamp', displayName: 'timestampLabel', @@ -109,54 +109,63 @@ export const createMockedRestrictedIndexPattern = () => ({ lang: 'painless', script: '1234', }, - ], - typeMeta: { - params: { - rollup_index: 'my-fake-index-pattern', - }, - aggs: { - terms: { - source: { - agg: 'terms', - }, + ]; + return { + id: '2', + title: 'my-fake-restricted-pattern', + timeFieldName: 'timestamp', + hasRestrictions: true, + fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } }, + fields, + getFieldByName: getFieldByNameFactory(fields), + typeMeta: { + params: { + rollup_index: 'my-fake-index-pattern', }, - date_histogram: { - timestamp: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', + aggs: { + terms: { + source: { + agg: 'terms', + }, }, - }, - histogram: { - bytes: { - agg: 'histogram', - interval: 1000, + date_histogram: { + timestamp: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, }, - }, - avg: { - bytes: { - agg: 'avg', + histogram: { + bytes: { + agg: 'histogram', + interval: 1000, + }, }, - }, - max: { - bytes: { - agg: 'max', + avg: { + bytes: { + agg: 'avg', + }, }, - }, - min: { - bytes: { - agg: 'min', + max: { + bytes: { + agg: 'max', + }, }, - }, - sum: { - bytes: { - agg: 'sum', + min: { + bytes: { + agg: 'min', + }, + }, + sum: { + bytes: { + agg: 'sum', + }, }, }, }, - }, -}); + }; +}; export function createMockedDragDropContext(): jest.Mocked { return { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index 65119d3978ee6..1cfa63511a45c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -43,7 +43,7 @@ export const cardinalityOperation: OperationDefinition { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index ac6bf63c37110..fc33b64ca508f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -18,6 +18,7 @@ import { } from '../../../../../../../src/plugins/data/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; +import { getFieldByNameFactory } from '../../pure_helpers'; const dataStart = dataPluginMock.createStartContract(); dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression( @@ -66,6 +67,17 @@ describe('date_histogram', () => { searchable: true, }, ], + + getFieldByName: getFieldByNameFactory([ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + }, + ]), }, 2: { id: '2', @@ -81,6 +93,16 @@ describe('date_histogram', () => { searchable: true, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'other_timestamp', + displayName: 'other_timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + }, + ]), }, }, layers: { @@ -267,6 +289,22 @@ describe('date_histogram', () => { }, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'timestamp', + displayName: 'timestamp', + aggregatable: true, + searchable: true, + type: 'date', + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'UTC', + calendar_interval: '42w', + }, + }, + }, + ]), } ); expect(esAggsConfig).toEqual( @@ -294,7 +332,7 @@ describe('date_histogram', () => { }, }; const indexPattern = createMockedIndexPattern(); - const newDateField = indexPattern.fields.find((i) => i.name === 'start_date')!; + const newDateField = indexPattern.getFieldByName('start_date')!; const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField); expect(column).toHaveProperty('sourceField', 'start_date'); @@ -314,7 +352,7 @@ describe('date_histogram', () => { }, }; const indexPattern = createMockedIndexPattern(); - const newDateField = indexPattern.fields.find((i) => i.name === 'start_date')!; + const newDateField = indexPattern.getFieldByName('start_date')!; const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField); expect(column).toHaveProperty('sourceField', 'start_date'); @@ -356,6 +394,22 @@ describe('date_histogram', () => { }, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'dateField', + displayName: 'dateField', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'CET', + calendar_interval: 'w', + }, + }, + }, + ]), } ); expect(transferedColumn).toEqual( @@ -393,6 +447,15 @@ describe('date_histogram', () => { searchable: true, }, ], + getFieldByName: getFieldByNameFactory([ + { + name: 'dateField', + displayName: 'dateField', + type: 'date', + aggregatable: true, + searchable: true, + }, + ]), } ); expect(transferedColumn).toEqual( @@ -609,6 +672,18 @@ describe('date_histogram', () => { }, }, ], + getFieldByName: getFieldByNameFactory([ + { + ...state.indexPatterns[1].fields[0], + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'UTC', + calendar_interval: '1h', + }, + }, + }, + ]), }, }, }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 185f44405bb4b..19043c03e5a61 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -81,7 +81,7 @@ export const dateHistogramOperation: OperationDefinition< }; }, isTransferable: (column, newIndexPattern) => { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && @@ -91,12 +91,9 @@ export const dateHistogramOperation: OperationDefinition< ); }, transfer: (column, newIndexPattern) => { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); - if ( - newField && - newField.aggregationRestrictions && - newField.aggregationRestrictions.date_histogram - ) { + const newField = newIndexPattern.getFieldByName(column.sourceField); + + if (newField?.aggregationRestrictions?.date_histogram) { const restrictions = newField.aggregationRestrictions.date_histogram; return { @@ -123,7 +120,7 @@ export const dateHistogramOperation: OperationDefinition< }; }, toEsAggsConfig: (column, columnId, indexPattern) => { - const usedField = indexPattern.fields.find((field) => field.name === column.sourceField); + const usedField = indexPattern.getFieldByName(column.sourceField); return { id: columnId, enabled: true, @@ -132,7 +129,7 @@ export const dateHistogramOperation: OperationDefinition< params: { field: column.sourceField, time_zone: column.params.timeZone, - useNormalizedEsInterval: !usedField || !usedField.aggregationRestrictions?.date_histogram, + useNormalizedEsInterval: !usedField?.aggregationRestrictions?.date_histogram, interval: column.params.interval, drop_partials: false, min_doc_count: 0, @@ -143,8 +140,8 @@ export const dateHistogramOperation: OperationDefinition< paramEditor: ({ state, setState, currentColumn, layerId, dateRange, data }) => { const field = currentColumn && - state.indexPatterns[state.layers[layerId].indexPatternId].fields.find( - (currentField) => currentField.name === currentColumn.sourceField + state.indexPatterns[state.layers[layerId].indexPatternId].getFieldByName( + currentColumn.sourceField ); const intervalIsRestricted = field!.aggregationRestrictions && field!.aggregationRestrictions.date_histogram; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 1d3ecc165ce74..fef575c61475c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -43,7 +43,7 @@ function buildMetricOperation>({ } }, isTransferable: (column, newIndexPattern) => { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index d43a905434c02..ce015284e544b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -30,6 +30,7 @@ import { } from './constants'; import { RangePopover } from './advanced_editor'; import { DragDropBuckets } from '../shared_components'; +import { getFieldByNameFactory } from '../../../pure_helpers'; const dataPluginMockValue = dataPluginMock.createStartContract(); // need to overwrite the formatter field first @@ -96,6 +97,9 @@ describe('ranges', () => { title: 'my_index_pattern', hasRestrictions: false, fields: [{ name: sourceField, type: 'number', displayName: sourceField }], + getFieldByName: getFieldByNameFactory([ + { name: sourceField, type: 'number', displayName: sourceField }, + ]), }, }, existingFields: {}, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index 46d9e4e6c22de..c6cc6ae13f178 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -140,7 +140,7 @@ export const rangeOperation: OperationDefinition { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && @@ -168,9 +168,7 @@ export const rangeOperation: OperationDefinition { const indexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; - const currentField = indexPattern.fields.find( - (field) => field.name === currentColumn.sourceField - ); + const currentField = indexPattern.getFieldByName(currentColumn.sourceField); const numberFormat = currentColumn.params.format; const numberFormatterPattern = numberFormat && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index dcb4646816e13..421068a5ad47f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -61,7 +61,7 @@ export const termsOperation: OperationDefinition { - const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + const newField = newIndexPattern.getFieldByName(column.sourceField); return Boolean( newField && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 1341ca0587c75..bb1b13ba74cc5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -103,7 +103,7 @@ describe('terms', () => { }, }; const indexPattern = createMockedIndexPattern(); - const newNumberField = indexPattern.fields.find((i) => i.name === 'bytes')!; + const newNumberField = indexPattern.getFieldByName('bytes')!; const column = termsOperation.onFieldChange(oldColumn, indexPattern, newNumberField); expect(column).toHaveProperty('dataType', 'number'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 6808bc724f26b..9767d4bdca688 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -8,38 +8,42 @@ import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColum import { AvgIndexPatternColumn } from './definitions/metrics'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; +import { getFieldByNameFactory } from '../pure_helpers'; jest.mock('../loader'); +const fields = [ + { + name: 'timestamp', + displayName: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, +]; + const expectedIndexPatterns = { 1: { id: '1', title: 'my-fake-index-pattern', timeFieldName: 'timestamp', hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestamp', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }, - ], + fields, + getFieldByName: getFieldByNameFactory(fields), }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts index 9e81b5e0c5bf9..c5da3d0c5dcde 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternPrivateState } from './types'; +import { keyBy } from 'lodash'; +import { IndexPatternField, IndexPatternPrivateState } from './types'; export function fieldExists( existingFields: IndexPatternPrivateState['existingFields'], @@ -13,3 +14,8 @@ export function fieldExists( ) { return existingFields[indexPatternTitle] && existingFields[indexPatternTitle][fieldName]; } + +export function getFieldByNameFactory(newFields: IndexPatternField[]) { + const fieldsLookup = keyBy(newFields, 'name'); + return (name: string) => fieldsLookup[name]; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index da90a2ce5fcec..45008b2d9439a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -16,6 +16,7 @@ import { TermsIndexPatternColumn } from './operations/definitions/terms'; import { DateHistogramIndexPatternColumn } from './operations/definitions/date_histogram'; import { AvgIndexPatternColumn } from './operations/definitions/metrics'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; +import { getFieldByNameFactory } from './pure_helpers'; jest.mock('./operations'); @@ -585,59 +586,61 @@ describe('state_helpers', () => { }); describe('updateLayerIndexPattern', () => { - const indexPattern: IndexPattern = { - id: 'test', - title: '', - hasRestrictions: true, - fields: [ - { - name: 'fieldA', - displayName: 'fieldA', - aggregatable: true, - searchable: true, - type: 'string', - }, - { - name: 'fieldB', - displayName: 'fieldB', - aggregatable: true, - searchable: true, - type: 'number', - aggregationRestrictions: { - avg: { - agg: 'avg', - }, + const fields = [ + { + name: 'fieldA', + displayName: 'fieldA', + aggregatable: true, + searchable: true, + type: 'string', + }, + { + name: 'fieldB', + displayName: 'fieldB', + aggregatable: true, + searchable: true, + type: 'number', + aggregationRestrictions: { + avg: { + agg: 'avg', }, }, - { - name: 'fieldC', - displayName: 'fieldC', - aggregatable: false, - searchable: true, - type: 'date', - }, - { - name: 'fieldD', - displayName: 'fieldD', - aggregatable: true, - searchable: true, - type: 'date', - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - time_zone: 'CET', - calendar_interval: 'w', - }, + }, + { + name: 'fieldC', + displayName: 'fieldC', + aggregatable: false, + searchable: true, + type: 'date', + }, + { + name: 'fieldD', + displayName: 'fieldD', + aggregatable: true, + searchable: true, + type: 'date', + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'CET', + calendar_interval: 'w', }, }, - { - name: 'fieldE', - displayName: 'fieldE', - aggregatable: true, - searchable: true, - type: 'date', - }, - ], + }, + { + name: 'fieldE', + displayName: 'fieldE', + aggregatable: true, + searchable: true, + type: 'date', + }, + ]; + const indexPattern: IndexPattern = { + id: 'test', + title: '', + hasRestrictions: true, + getFieldByName: getFieldByNameFactory(fields), + fields, }; it('should switch index pattern id in layer', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index a3c0e8aed7421..1e6fc5a5806b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -11,6 +11,7 @@ import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/pub export interface IndexPattern { id: string; fields: IndexPatternField[]; + getFieldByName(name: string): IndexPatternField | undefined; title: string; timeFieldName?: string; fieldFormatMap?: Record< diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index d3d65617f2253..d0ea81d135156 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -87,15 +87,15 @@ export function fieldIsInvalid( indexPattern: IndexPattern ) { const operationDefinition = operationType && operationDefinitionMap[operationType]; + const field = sourceField ? indexPattern.getFieldByName(sourceField) : undefined; return Boolean( sourceField && operationDefinition && - !indexPattern.fields.some( - (field) => - field.name === sourceField && - operationDefinition?.input === 'field' && - operationDefinition.getPossibleOperationForField(field) !== undefined + !( + field && + operationDefinition?.input === 'field' && + operationDefinition.getPossibleOperationForField(field) !== undefined ) ); }