diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 5e917aee7ae9f..0076a9f599bb9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -18,12 +18,7 @@ import { OperationType, } from '../indexpattern'; -import { - getAvailableOperationsByMetadata, - buildColumn, - operationDefinitionMap, - OperationDefinition, -} from '../operations'; +import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations'; import { PopoverEditor } from './popover_editor'; import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_drop'; import { changeColumn, deleteColumn } from '../state_helpers'; @@ -130,9 +125,7 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan // If only the field has changed use the onFieldChange method on the operation to get the // new column, otherwise use the regular buildColumn to get a new column. const newColumn = hasFieldChanged - ? (operationDefinitionMap[selectedColumn.operationType] as OperationDefinition< - IndexPatternColumn - >).onFieldChange(selectedColumn, currentIndexPattern, droppedItem.field) + ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) : buildColumn({ columns: props.state.layers[props.layerId].columns, indexPattern: currentIndexPattern, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index 4b476936f1f76..960e81b98699b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -32,7 +32,7 @@ import { operationDefinitionMap, getOperationDisplay, buildColumn, - OperationDefinition, + changeField, } from '../operations'; import { deleteColumn, changeColumn } from '../state_helpers'; import { FieldSelect } from './field_select'; @@ -284,14 +284,7 @@ export function PopoverEditor(props: PopoverEditorProps) { ) { // 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. - const operation = operationDefinitionMap[ - choice.operationType - ] as OperationDefinition; - column = operation.onFieldChange( - selectedColumn, - currentIndexPattern, - fieldMap[choice.field] - ); + column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); } else { // Otherwise we'll use the buildColumn method to calculate a new column column = buildColumn({ @@ -358,6 +351,7 @@ export function PopoverEditor(props: PopoverEditorProps) { state={state} setState={setState} columnId={columnId} + currentColumn={state.layers[layerId].columns[columnId]} storage={props.storage} uiSettings={props.uiSettings} layerId={layerId} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 388d4de25a792..a7cbdd59c6e0f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -12,11 +12,9 @@ import { Storage } from 'ui/storage'; import { DatasourceDimensionPanelProps, DatasourceDataPanelProps, - DimensionPriority, Operation, DatasourceLayerPanelProps, } from '../types'; -import { Query } from '../../../../../../src/legacy/core_plugins/data/public/query'; import { getIndexPatterns } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; @@ -29,70 +27,10 @@ import { import { isDraggedField } from './utils'; import { LayerPanel } from './layerpanel'; +import { IndexPatternColumn } from './operations'; import { Datasource } from '..'; -export type OperationType = IndexPatternColumn['operationType']; - -export type IndexPatternColumn = - | DateHistogramIndexPatternColumn - | TermsIndexPatternColumn - | SumIndexPatternColumn - | AvgIndexPatternColumn - | MinIndexPatternColumn - | MaxIndexPatternColumn - | CountIndexPatternColumn - | FilterRatioIndexPatternColumn; - -export interface BaseIndexPatternColumn extends Operation { - // Private - operationType: OperationType; - suggestedPriority?: DimensionPriority; -} - -type Omit = Pick>; -type ParameterlessIndexPatternColumn< - TOperationType extends OperationType, - TBase extends BaseIndexPatternColumn = FieldBasedIndexPatternColumn -> = Omit & { operationType: TOperationType }; - -export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn { - sourceField: string; - suggestedPriority?: DimensionPriority; -} - -export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternColumn { - operationType: 'date_histogram'; - params: { - interval: string; - timeZone?: string; - }; -} - -export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { - operationType: 'terms'; - params: { - size: number; - orderBy: { type: 'alphabetical' } | { type: 'column'; columnId: string }; - orderDirection: 'asc' | 'desc'; - }; -} - -export interface FilterRatioIndexPatternColumn extends BaseIndexPatternColumn { - operationType: 'filter_ratio'; - params: { - numerator: Query; - denominator: Query; - }; -} - -export type CountIndexPatternColumn = ParameterlessIndexPatternColumn< - 'count', - BaseIndexPatternColumn ->; -export type SumIndexPatternColumn = ParameterlessIndexPatternColumn<'sum'>; -export type AvgIndexPatternColumn = ParameterlessIndexPatternColumn<'avg'>; -export type MinIndexPatternColumn = ParameterlessIndexPatternColumn<'min'>; -export type MaxIndexPatternColumn = ParameterlessIndexPatternColumn<'max'>; +export { OperationType, IndexPatternColumn } from './operations'; export interface IndexPattern { id: string; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts deleted file mode 100644 index af8e2aaeb0380..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import { Storage } from 'ui/storage'; -import { UiSettingsClientContract } from 'src/core/public'; -import { DimensionPriority, OperationMetadata, StateSetter } from '../types'; -import { - IndexPatternColumn, - IndexPatternField, - IndexPatternPrivateState, - OperationType, - BaseIndexPatternColumn, - IndexPattern, -} from './indexpattern'; -import { termsOperation } from './operation_definitions/terms'; -import { - minOperation, - averageOperation, - sumOperation, - maxOperation, -} from './operation_definitions/metrics'; -import { dateHistogramOperation } from './operation_definitions/date_histogram'; -import { countOperation } from './operation_definitions/count'; -import { filterRatioOperation } from './operation_definitions/filter_ratio'; - -type PossibleOperationDefinition< - U extends IndexPatternColumn = IndexPatternColumn -> = U extends IndexPatternColumn ? OperationDefinition : never; - -type PossibleOperationDefinitionMapEntyries< - U extends PossibleOperationDefinition = PossibleOperationDefinition -> = U extends PossibleOperationDefinition ? { [K in U['type']]: U } : never; - -type UnionToIntersection = (U extends U ? (k: U) => void : never) extends ((k: infer I) => void) - ? I - : never; - -// this type makes sure that there is an operation definition for each column type -export type AllOperationDefinitions = UnionToIntersection; - -export const operationDefinitionMap: AllOperationDefinitions = { - terms: termsOperation, - date_histogram: dateHistogramOperation, - min: minOperation, - max: maxOperation, - avg: averageOperation, - sum: sumOperation, - count: countOperation, - filter_ratio: filterRatioOperation, -}; -const operationDefinitions: PossibleOperationDefinition[] = Object.values(operationDefinitionMap); - -export function getOperations(): OperationType[] { - return Object.keys(operationDefinitionMap) as OperationType[]; -} - -export interface ParamEditorProps { - state: IndexPatternPrivateState; - setState: StateSetter; - columnId: string; - layerId: string; - uiSettings: UiSettingsClientContract; - storage: Storage; -} - -export interface OperationDefinition { - type: C['operationType']; - displayName: string; - getPossibleOperationsForDocument: (indexPattern: IndexPattern) => OperationMetadata[]; - getPossibleOperationsForField: (field: IndexPatternField) => OperationMetadata[]; - buildColumn: (arg: { - suggestedPriority: DimensionPriority | undefined; - layerId: string; - columns: Partial>; - field?: IndexPatternField; - indexPattern: IndexPattern; - }) => C; - onOtherColumnChanged?: ( - currentColumn: C, - columns: Partial> - ) => C; - paramEditor?: React.ComponentType; - toEsAggsConfig: (column: C, columnId: string) => unknown; - isTransferable: (column: C, newIndexPattern: IndexPattern) => boolean; - transfer?: (column: C, newIndexPattern: IndexPattern) => C; - /** - * This method will be called if the user changes the field of an operation. - * You must implement it and return the new column after the field change. - * The most simple implementation will just change the field on the column, and keep - * the rest the same. Some implementations might want to change labels, or their parameters - * when changing the field. - * - * This will only be called for switching the field, not for initially selecting a field. - * - * See {@link OperationDefinition#transfer} for controlling column building when switching an - * index pattern not just a field. - * - * @param oldColumn The column before the user changed the field. - * @param indexPattern The index pattern that field is on. - * @param field The field that the user changed to. - */ - onFieldChange: (oldColumn: C, indexPattern: IndexPattern, field: IndexPatternField) => C; -} - -export function isColumnTransferable(column: IndexPatternColumn, newIndexPattern: IndexPattern) { - return (operationDefinitionMap[column.operationType] as OperationDefinition< - IndexPatternColumn - >).isTransferable(column, newIndexPattern); -} - -export function getOperationDisplay() { - const display = {} as Record< - OperationType, - { - type: OperationType; - displayName: string; - } - >; - operationDefinitions.forEach(({ type, displayName }) => { - display[type] = { - type, - displayName, - }; - }); - return display; -} - -export function getOperationTypesForField(field: IndexPatternField) { - return operationDefinitions - .filter( - operationDefinition => operationDefinition.getPossibleOperationsForField(field).length > 0 - ) - .map(({ type }) => type); -} - -type OperationFieldTuple = - | { type: 'field'; operationType: OperationType; field: string } - | { operationType: OperationType; type: 'document' }; - -export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { - const operationByMetadata: Record< - string, - { operationMetaData: OperationMetadata; operations: OperationFieldTuple[] } - > = {}; - - const addToMap = (operation: OperationFieldTuple) => (operationMetadata: OperationMetadata) => { - const key = JSON.stringify(operationMetadata); - - if (operationByMetadata[key]) { - operationByMetadata[key].operations.push(operation); - } else { - operationByMetadata[key] = { - operationMetaData: operationMetadata, - operations: [operation], - }; - } - }; - - operationDefinitions.forEach(operationDefinition => { - operationDefinition - .getPossibleOperationsForDocument(indexPattern) - .forEach(addToMap({ type: 'document', operationType: operationDefinition.type })); - - indexPattern.fields.forEach(field => { - operationDefinition.getPossibleOperationsForField(field).forEach( - addToMap({ - type: 'field', - operationType: operationDefinition.type, - field: field.name, - }) - ); - }); - }); - - return Object.values(operationByMetadata); -} - -export function buildColumn({ - op, - columns, - field, - layerId, - indexPattern, - suggestedPriority, - asDocumentOperation, -}: { - op?: OperationType; - columns: Partial>; - suggestedPriority: DimensionPriority | undefined; - layerId: string; - indexPattern: IndexPattern; - field?: IndexPatternField; - asDocumentOperation?: boolean; -}): IndexPatternColumn { - let operationDefinition: PossibleOperationDefinition; - - if (op) { - operationDefinition = operationDefinitionMap[op]; - } else if (asDocumentOperation) { - operationDefinition = operationDefinitions.find( - definition => definition.getPossibleOperationsForDocument(indexPattern).length !== 0 - )!; - } else if (field) { - operationDefinition = operationDefinitions.find( - definition => definition.getPossibleOperationsForField(field).length !== 0 - )!; - } - return operationDefinition!.buildColumn({ - columns, - suggestedPriority, - field, - layerId, - indexPattern, - }); -} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts similarity index 86% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts index a7ab5fc2c6faa..eeb19bba24006 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -const actual = jest.requireActual('../operations'); +const actual = jest.requireActual('../../operations'); jest.spyOn(actual.operationDefinitionMap.date_histogram, 'paramEditor'); jest.spyOn(actual.operationDefinitionMap.terms, 'onOtherColumnChanged'); @@ -17,5 +17,7 @@ export const { getOperationTypesForField, getOperationResultType, operationDefinitionMap, + operationDefinitions, isColumnTransferable, + changeField, } = actual; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts new file mode 100644 index 0000000000000..ed0e2fb3c96c5 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Operation, DimensionPriority } from '../../../types'; + +/** + * This is the root type of a column. If you are implementing a new + * operation, extend your column type on `BaseIndexPatternColumn` to make + * sure it's matching all the basic requirements. + */ +export interface BaseIndexPatternColumn extends Operation { + // Private + operationType: string; + suggestedPriority?: DimensionPriority; +} + +/** + * Base type for a column that doesn't have additional parameter. + * + * * `TOperationType` should be a string type containing just the type + * of the operation (e.g. `"sum"`). + * * `TBase` is the base column interface the operation type is set for - + * by default this is `FieldBasedIndexPatternColumn`, so + * `ParameterlessIndexPatternColumn<'foo'>` will give you a column type + * for an operation named foo that operates on a field. + * By passing in another `TBase` (e.g. just `BaseIndexPatternColumn`), + * you can also create other column types. + */ +export type ParameterlessIndexPatternColumn< + TOperationType extends string, + TBase extends BaseIndexPatternColumn = FieldBasedIndexPatternColumn +> = TBase & { operationType: TOperationType }; + +export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn { + sourceField: string; + suggestedPriority?: DimensionPriority; +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx similarity index 68% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx index d751b944cebe2..8a47e68a279ad 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx @@ -5,28 +5,30 @@ */ import { i18n } from '@kbn/i18n'; -import { CountIndexPatternColumn } from '../indexpattern'; -import { OperationDefinition } from '../operations'; +import { OperationDefinition } from '.'; +import { ParameterlessIndexPatternColumn, BaseIndexPatternColumn } from './column_types'; const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', { defaultMessage: 'Count of documents', }); +export type CountIndexPatternColumn = ParameterlessIndexPatternColumn< + 'count', + BaseIndexPatternColumn +>; + export const countOperation: OperationDefinition = { type: 'count', displayName: i18n.translate('xpack.lens.indexPattern.count', { defaultMessage: 'Count', }), - getPossibleOperationsForField: () => [], - getPossibleOperationsForDocument: () => { - return [ - { - dataType: 'number', - isBucketed: false, - isMetric: true, - scale: 'ratio', - }, - ]; + getPossibleOperationForDocument: () => { + return { + dataType: 'number', + isBucketed: false, + isMetric: true, + scale: 'ratio', + }; }, buildColumn({ suggestedPriority }) { return { @@ -39,8 +41,6 @@ export const countOperation: OperationDefinition = { scale: 'ratio', }; }, - // This cannot be called practically, since this is a fieldless operation - onFieldChange: oldColumn => ({ ...oldColumn }), toEsAggsConfig: (column, columnId) => ({ id: columnId, enabled: true, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx index f7a758febd8d2..12015a281eaa9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; -import { dateHistogramOperation } from './date_histogram'; +import { DateHistogramIndexPatternColumn } from './date_histogram'; +import { dateHistogramOperation } from '.'; import { shallow } from 'enzyme'; -import { DateHistogramIndexPatternColumn, IndexPatternPrivateState } from '../indexpattern'; +import { IndexPatternPrivateState } from '../../indexpattern'; import { EuiRange, EuiSwitch } from '@elastic/eui'; import { UiSettingsClientContract } from 'src/core/public'; import { Storage } from 'ui/storage'; -import { createMockedIndexPattern } from '../mocks'; +import { createMockedIndexPattern } from '../../mocks'; jest.mock('ui/new_platform'); @@ -325,6 +326,7 @@ describe('date_histogram', () => { setState={setStateSpy} columnId="col1" layerId="first" + currentColumn={state.layers.first.columns.col1 as DateHistogramIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> @@ -340,6 +342,7 @@ describe('date_histogram', () => { state={state} setState={setStateSpy} columnId="col2" + currentColumn={state.layers.second.columns.col2 as DateHistogramIndexPatternColumn} layerId="second" storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} @@ -355,6 +358,7 @@ describe('date_histogram', () => { state={state} setState={jest.fn()} columnId="col1" + currentColumn={state.layers.third.columns.col1 as DateHistogramIndexPatternColumn} layerId="third" storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} @@ -372,6 +376,7 @@ describe('date_histogram', () => { setState={setStateSpy} columnId="col1" layerId="third" + currentColumn={state.layers.third.columns.col1 as DateHistogramIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> @@ -392,6 +397,7 @@ describe('date_histogram', () => { setState={setStateSpy} columnId="col1" layerId="first" + currentColumn={state.layers.first.columns.col1 as DateHistogramIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> @@ -449,6 +455,7 @@ describe('date_histogram', () => { setState={setStateSpy} columnId="col1" layerId="first" + currentColumn={state.layers.first.columns.col1 as DateHistogramIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx similarity index 83% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx index 92cb474abe9d2..6c75141388514 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx @@ -8,9 +8,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiForm, EuiFormRow, EuiRange, EuiSwitch } from '@elastic/eui'; -import { DateHistogramIndexPatternColumn, IndexPattern } from '../indexpattern'; -import { OperationDefinition } from '../operations'; -import { updateColumnParam } from '../state_helpers'; +import { IndexPattern } from '../../indexpattern'; +import { updateColumnParam } from '../../state_helpers'; +import { OperationDefinition } from '.'; +import { FieldBasedIndexPatternColumn } from './column_types'; type PropType = C extends React.ComponentType ? P : unknown; @@ -39,33 +40,34 @@ function supportsAutoInterval(fieldName: string, indexPattern: IndexPattern): bo return indexPattern.timeFieldName ? indexPattern.timeFieldName === fieldName : false; } +export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'date_histogram'; + params: { + interval: string; + timeZone?: string; + }; +} + export const dateHistogramOperation: OperationDefinition = { type: 'date_histogram', displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', { defaultMessage: 'Date Histogram', }), - getPossibleOperationsForDocument: () => [], - getPossibleOperationsForField: ({ aggregationRestrictions, aggregatable, type }) => { + getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( type === 'date' && aggregatable && (!aggregationRestrictions || aggregationRestrictions.date_histogram) ) { - return [ - { - dataType: 'date', - isBucketed: true, - isMetric: false, - scale: 'interval', - }, - ]; + return { + dataType: 'date', + isBucketed: true, + isMetric: false, + scale: 'interval', + }; } - return []; }, - buildColumn({ suggestedPriority, field, indexPattern }): DateHistogramIndexPatternColumn { - if (!field) { - throw new Error('Invariant error: date histogram buildColumn requires field'); - } + buildColumn({ suggestedPriority, field, indexPattern }) { let interval = indexPattern.timeFieldName === field.name ? autoInterval : defaultCustomInterval; let timeZone: string | undefined; if (field.aggregationRestrictions && field.aggregationRestrictions.date_histogram) { @@ -164,13 +166,11 @@ export const dateHistogramOperation: OperationDefinition { - const column = state.layers[layerId].columns[columnId] as DateHistogramIndexPatternColumn; - + paramEditor: ({ state, setState, currentColumn: currentColumn, layerId }) => { const field = - column && + currentColumn && state.indexPatterns[state.layers[layerId].indexPatternId].fields.find( - currentField => currentField.name === column.sourceField + currentField => currentField.name === currentColumn.sourceField ); const intervalIsRestricted = field!.aggregationRestrictions && field!.aggregationRestrictions.date_histogram; @@ -187,7 +187,9 @@ export const dateHistogramOperation: OperationDefinition) { const interval = ev.target.checked ? defaultCustomInterval : autoInterval; - setState(updateColumnParam(state, layerId, column, 'interval', interval)); + setState( + updateColumnParam({ state, layerId, currentColumn, paramName: 'interval', value: interval }) + ); } return ( @@ -198,12 +200,12 @@ export const dateHistogramOperation: OperationDefinition )} - {column.params.interval !== autoInterval && ( + {currentColumn.params.interval !== autoInterval && ( ) : ( @@ -222,7 +224,7 @@ export const dateHistogramOperation: OperationDefinition ({ label: interval, @@ -230,13 +232,13 @@ export const dateHistogramOperation: OperationDefinition) => setState( - updateColumnParam( + updateColumnParam({ state, layerId, - column, - 'interval', - numericToInterval(Number(e.target.value)) - ) + currentColumn, + paramName: 'interval', + value: numericToInterval(Number(e.target.value)), + }) ) } aria-label={i18n.translate('xpack.lens.indexPattern.dateHistogram.interval', { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx similarity index 89% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx index 51d8497cde1bf..94fe425543936 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx @@ -7,12 +7,13 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { act } from 'react-dom/test-utils'; -import { filterRatioOperation } from './filter_ratio'; -import { FilterRatioIndexPatternColumn, IndexPatternPrivateState } from '../indexpattern'; +import { FilterRatioIndexPatternColumn } from './filter_ratio'; +import { filterRatioOperation } from '.'; +import { IndexPatternPrivateState } from '../../indexpattern'; import { Storage } from 'ui/storage'; import { UiSettingsClientContract } from 'src/core/public'; -import { QueryBarInput } from '../../../../../../../src/legacy/core_plugins/data/public/query'; -import { createMockedIndexPattern } from '../mocks'; +import { QueryBarInput } from '../../../../../../../../src/legacy/core_plugins/data/public/query'; +import { createMockedIndexPattern } from '../../mocks'; jest.mock('ui/new_platform'); @@ -104,6 +105,7 @@ describe('filter_ratio', () => { state={state} setState={jest.fn()} columnId="col1" + currentColumn={state.layers.first.columns.col1 as FilterRatioIndexPatternColumn} storage={storageMock} uiSettings={{} as UiSettingsClientContract} /> @@ -118,6 +120,7 @@ describe('filter_ratio', () => { state={state} setState={jest.fn()} columnId="col1" + currentColumn={state.layers.first.columns.col1 as FilterRatioIndexPatternColumn} storage={storageMock} uiSettings={{} as UiSettingsClientContract} /> @@ -135,6 +138,7 @@ describe('filter_ratio', () => { state={state} setState={setState} columnId="col1" + currentColumn={state.layers.first.columns.col1 as FilterRatioIndexPatternColumn} storage={storageMock} uiSettings={{} as UiSettingsClientContract} /> @@ -173,6 +177,7 @@ describe('filter_ratio', () => { state={state} setState={setState} columnId="col1" + currentColumn={state.layers.first.columns.col1 as FilterRatioIndexPatternColumn} storage={storageMock} uiSettings={{} as UiSettingsClientContract} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx similarity index 72% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx index 025bbf14b7e87..6dee938b1fce9 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx @@ -11,30 +11,35 @@ import { EuiButton, EuiFormRow } from '@elastic/eui'; import { Query, QueryBarInput, -} from '../../../../../../../src/legacy/core_plugins/data/public/query'; -import { FilterRatioIndexPatternColumn } from '../indexpattern'; -import { OperationDefinition } from '../operations'; -import { updateColumnParam } from '../state_helpers'; +} from '../../../../../../../../src/legacy/core_plugins/data/public/query'; +import { updateColumnParam } from '../../state_helpers'; +import { OperationDefinition } from '.'; +import { BaseIndexPatternColumn } from './column_types'; const filterRatioLabel = i18n.translate('xpack.lens.indexPattern.filterRatio', { defaultMessage: 'Filter Ratio', }); +export interface FilterRatioIndexPatternColumn extends BaseIndexPatternColumn { + operationType: 'filter_ratio'; + params: { + numerator: Query; + denominator: Query; + }; +} + export const filterRatioOperation: OperationDefinition = { type: 'filter_ratio', displayName: i18n.translate('xpack.lens.indexPattern.filterRatio', { defaultMessage: 'Filter Ratio', }), - getPossibleOperationsForField: () => [], - getPossibleOperationsForDocument: () => { - return [ - { - dataType: 'number', - isBucketed: false, - isMetric: true, - scale: 'ratio', - }, - ]; + getPossibleOperationForDocument: () => { + return { + dataType: 'number', + isBucketed: false, + isMetric: true, + scale: 'ratio', + }; }, buildColumn({ suggestedPriority }) { return { @@ -51,8 +56,6 @@ export const filterRatioOperation: OperationDefinition ({ ...oldColumn }), toEsAggsConfig: (column, columnId) => ({ id: columnId, enabled: true, @@ -75,7 +78,7 @@ export const filterRatioOperation: OperationDefinition { + paramEditor: ({ state, setState, currentColumn, uiSettings, storage, layerId }) => { const [hasDenominator, setDenominator] = useState(false); return ( @@ -88,22 +91,19 @@ export const filterRatioOperation: OperationDefinition { setState( - updateColumnParam( + updateColumnParam({ state, layerId, - state.layers[layerId].columns[currentColumnId] as FilterRatioIndexPatternColumn, - 'numerator', - newQuery - ) + currentColumn, + paramName: 'numerator', + value: newQuery, + }) ); }} /> @@ -118,22 +118,19 @@ export const filterRatioOperation: OperationDefinition { setState( - updateColumnParam( + updateColumnParam({ state, layerId, - state.layers[layerId].columns[currentColumnId] as FilterRatioIndexPatternColumn, - 'denominator', - newQuery - ) + currentColumn, + paramName: 'denominator', + value: newQuery, + }) ); }} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts new file mode 100644 index 0000000000000..b7ea189beb2c9 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Storage } from 'ui/storage'; +import { UiSettingsClientContract } from 'src/core/public'; +import { termsOperation } from './terms'; +import { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; +import { dateHistogramOperation } from './date_histogram'; +import { countOperation } from './count'; +import { filterRatioOperation } from './filter_ratio'; +import { DimensionPriority, StateSetter, OperationMetadata } from '../../../types'; +import { BaseIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternField } from '../../indexpattern'; + +// List of all operation definitions registered to this data source. +// If you want to implement a new operation, add it to this array and +// its type will get propagated to everything else +const internalOperationDefinitions = [ + termsOperation, + dateHistogramOperation, + minOperation, + maxOperation, + averageOperation, + sumOperation, + countOperation, + filterRatioOperation, +]; + +export { termsOperation } from './terms'; +export { dateHistogramOperation } from './date_histogram'; +export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; +export { countOperation } from './count'; +export { filterRatioOperation } from './filter_ratio'; + +/** + * Properties passed to the operation-specific part of the popover editor + */ +export interface ParamEditorProps { + currentColumn: C; + state: IndexPatternPrivateState; + setState: StateSetter; + columnId: string; + layerId: string; + uiSettings: UiSettingsClientContract; + storage: Storage; +} + +interface BaseOperationDefinitionProps { + type: C['operationType']; + /** + * The name of the operation shown to the user (e.g. in the popover editor). + * Should be i18n-ified. + */ + displayName: string; + /** + * This function is called if another column in the same layer changed or got removed. + * Can be used to update references to other columns (e.g. for sorting). + * Based on the current column and the other updated columns, this function has to + * return an updated column. If not implemented, the `id` function is used instead. + */ + onOtherColumnChanged?: ( + currentColumn: C, + columns: Partial> + ) => C; + /** + * React component for operation specific settings shown in the popover editor + */ + paramEditor?: React.ComponentType>; + /** + * Function turning a column into an agg config passed to the `esaggs` function + * together with the agg configs returned from other columns. + */ + toEsAggsConfig: (column: C, columnId: string) => unknown; + /** + * Returns true if the `column` can also be used on `newIndexPattern`. + * If this function returns false, the column is removed when switching index pattern + * for a layer + */ + isTransferable: (column: C, newIndexPattern: IndexPattern) => boolean; + /** + * Transfering a column to another index pattern. This can be used to + * adjust operation specific settings such as reacting to aggregation restrictions + * present on the new index pattern. + */ + transfer?: (column: C, newIndexPattern: IndexPattern) => C; +} + +interface BaseBuildColumnArgs { + suggestedPriority: DimensionPriority | undefined; + layerId: string; + columns: Partial>; + indexPattern: IndexPattern; +} + +interface FieldBasedOperationDefinition + extends BaseOperationDefinitionProps { + /** + * Returns the meta data of the operation if applied to the given field. Undefined + * if the field is not applicable to the operation. + */ + getPossibleOperationForField: (field: IndexPatternField) => OperationMetadata | undefined; + /** + * Builds the column object for the given parameters. Should include default p + */ + buildColumn: ( + arg: BaseBuildColumnArgs & { + field: IndexPatternField; + } + ) => C; + /** + * This method will be called if the user changes the field of an operation. + * You must implement it and return the new column after the field change. + * The most simple implementation will just change the field on the column, and keep + * the rest the same. Some implementations might want to change labels, or their parameters + * when changing the field. + * + * This will only be called for switching the field, not for initially selecting a field. + * + * See {@link OperationDefinition#transfer} for controlling column building when switching an + * index pattern not just a field. + * + * @param oldColumn The column before the user changed the field. + * @param indexPattern The index pattern that field is on. + * @param field The field that the user changed to. + */ + onFieldChange: (oldColumn: C, indexPattern: IndexPattern, field: IndexPatternField) => C; +} + +interface DocumentBasedOperationDefinition + extends BaseOperationDefinitionProps { + /** + * Returns the meta data of the operation if applied to documents of the given index pattern. + * Undefined if the operation is not applicable to the index pattern. + */ + getPossibleOperationForDocument: (indexPattern: IndexPattern) => OperationMetadata | undefined; + buildColumn: (arg: BaseBuildColumnArgs) => C; +} + +/** + * Shape of an operation definition. If the type parameter of the definition + * indicates a field based column, `getPossibleOperationForField` has to be + * specified, otherwise `getPossibleOperationForDocument` has to be defined. + */ +export type OperationDefinition< + C extends BaseIndexPatternColumn +> = C extends FieldBasedIndexPatternColumn + ? FieldBasedOperationDefinition + : DocumentBasedOperationDefinition; + +// Helper to to infer the column type out of the operation definition. +// This is done to avoid it to have to list out the column types along with +// the operation definition types +type ColumnFromOperationDefinition = D extends OperationDefinition ? C : never; + +/** + * A union type of all available column types. If a column is of an unknown type somewhere + * withing the indexpattern data source it should be typed as `IndexPatternColumn` to make + * typeguards possible that consider all available column types. + */ +export type IndexPatternColumn = ColumnFromOperationDefinition< + (typeof internalOperationDefinitions)[number] +>; + +/** + * A union type of all available operation types. The operation type is a unique id of an operation. + * Each column is assigned to exactly one operation type. + */ +export type OperationType = (typeof internalOperationDefinitions)[number]['type']; + +/** + * This is an operation definition of an unspecified column out of all possible + * column types. It + */ +export type GenericOperationDefinition = + | FieldBasedOperationDefinition + | DocumentBasedOperationDefinition; + +/** + * List of all available operation definitions + */ +export const operationDefinitions = internalOperationDefinitions as GenericOperationDefinition[]; + +/** + * Map of all operation visible to consumers (e.g. the dimension panel). + * This simplifies the type of the map and makes it a simple list of unspecified + * operations definitions, because typescript can't infer the type correctly in most + * situations. + * + * If you need a specifically typed version of an operation (e.g. explicitly working with terms), + * you should import the definition directly from this file + * (e.g. `import { termsOperation } from './operations/definitions'`). This map is + * intended to be used in situations where the operation type is not known during compile time. + */ +export const operationDefinitionMap = internalOperationDefinitions.reduce( + (definitionMap, definition) => ({ ...definitionMap, [definition.type]: definition }), + {} +) as Record; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx similarity index 68% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx index aadca0f1f7dbd..f33bd1cfd3967 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx @@ -5,40 +5,30 @@ */ import { i18n } from '@kbn/i18n'; -import { - FieldBasedIndexPatternColumn, - MinIndexPatternColumn, - SumIndexPatternColumn, - AvgIndexPatternColumn, - MaxIndexPatternColumn, -} from '../indexpattern'; -import { OperationDefinition } from '../operations'; +import { OperationDefinition } from '.'; +import { ParameterlessIndexPatternColumn } from './column_types'; -function buildMetricOperation( +function buildMetricOperation>( type: T['operationType'], displayName: string, ofName: (name: string) => string ) { - const operationDefinition: OperationDefinition = { + return { type, displayName, - getPossibleOperationsForDocument: () => [], - getPossibleOperationsForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => { + getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => { if ( fieldType === 'number' && aggregatable && (!aggregationRestrictions || aggregationRestrictions[type]) ) { - return [ - { - dataType: 'number', - isBucketed: false, - isMetric: true, - scale: 'ratio', - }, - ]; + return { + dataType: 'number', + isBucketed: false, + isMetric: true, + scale: 'ratio', + }; } - return []; }, isTransferable: (column, newIndexPattern) => { const newField = newIndexPattern.fields.find(field => field.name === column.sourceField); @@ -50,21 +40,16 @@ function buildMetricOperation( (!newField.aggregationRestrictions || newField.aggregationRestrictions![type]) ); }, - buildColumn({ suggestedPriority, field }): T { - if (!field) { - throw new Error(`Invariant: A ${type} operation can only be built with a field`); - } - return { - label: ofName(field ? field.name : ''), - dataType: 'number', - operationType: type, - suggestedPriority, - sourceField: field ? field.name : '', - isBucketed: false, - isMetric: true, - scale: 'ratio', - } as T; - }, + buildColumn: ({ suggestedPriority, field }) => ({ + label: ofName(field ? field.name : ''), + dataType: 'number', + operationType: type, + suggestedPriority, + sourceField: field ? field.name : '', + isBucketed: false, + isMetric: true, + scale: 'ratio', + }), onFieldChange: (oldColumn, indexPattern, field) => { return { ...oldColumn, @@ -81,10 +66,14 @@ function buildMetricOperation( field: column.sourceField, }, }), - }; - return operationDefinition; + } as OperationDefinition; } +export type SumIndexPatternColumn = ParameterlessIndexPatternColumn<'sum'>; +export type AvgIndexPatternColumn = ParameterlessIndexPatternColumn<'avg'>; +export type MinIndexPatternColumn = ParameterlessIndexPatternColumn<'min'>; +export type MaxIndexPatternColumn = ParameterlessIndexPatternColumn<'max'>; + export const minOperation = buildMetricOperation( 'min', i18n.translate('xpack.lens.indexPattern.min', { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx similarity index 91% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx index 4ce5d4bd32223..b6883a0cc3709 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; -import { termsOperation } from './terms'; import { shallow } from 'enzyme'; -import { IndexPatternPrivateState, TermsIndexPatternColumn } from '../indexpattern'; +import { IndexPatternPrivateState } from '../../indexpattern'; import { EuiRange, EuiSelect } from '@elastic/eui'; import { UiSettingsClientContract } from 'src/core/public'; import { Storage } from 'ui/storage'; -import { createMockedIndexPattern } from '../mocks'; +import { createMockedIndexPattern } from '../../mocks'; +import { TermsIndexPatternColumn } from './terms'; +import { termsOperation } from '.'; jest.mock('ui/new_platform'); @@ -105,10 +106,10 @@ describe('terms', () => { }); }); - describe('getPossibleOperationsForField', () => { + describe('getPossibleOperationForField', () => { it('should return operation with the right type', () => { expect( - termsOperation.getPossibleOperationsForField({ + termsOperation.getPossibleOperationForField({ aggregatable: true, searchable: true, name: 'test', @@ -119,51 +120,47 @@ describe('terms', () => { }, }, }) - ).toEqual([ - { - dataType: 'string', - isBucketed: true, - isMetric: false, - scale: 'ordinal', - }, - ]); + ).toEqual({ + dataType: 'string', + isBucketed: true, + isMetric: false, + scale: 'ordinal', + }); expect( - termsOperation.getPossibleOperationsForField({ + termsOperation.getPossibleOperationForField({ aggregatable: true, searchable: true, name: 'test', type: 'boolean', }) - ).toEqual([ - { - dataType: 'boolean', - isBucketed: true, - isMetric: false, - scale: 'ordinal', - }, - ]); + ).toEqual({ + dataType: 'boolean', + isBucketed: true, + isMetric: false, + scale: 'ordinal', + }); }); it('should not return an operation if restrictions prevent terms', () => { expect( - termsOperation.getPossibleOperationsForField({ + termsOperation.getPossibleOperationForField({ aggregatable: false, searchable: true, name: 'test', type: 'string', }) - ).toEqual([]); + ).toEqual(undefined); expect( - termsOperation.getPossibleOperationsForField({ + termsOperation.getPossibleOperationForField({ aggregatable: true, aggregationRestrictions: {}, searchable: true, name: 'test', type: 'string', }) - ).toEqual([]); + ).toEqual(undefined); }); }); @@ -321,6 +318,7 @@ describe('terms', () => { state={state} setState={setStateSpy} columnId="col1" + currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn} layerId="first" storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} @@ -368,6 +366,7 @@ describe('terms', () => { setState={setStateSpy} columnId="col1" layerId="first" + currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> @@ -385,6 +384,7 @@ describe('terms', () => { state={state} setState={setStateSpy} columnId="col1" + currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn} layerId="first" storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} @@ -431,6 +431,7 @@ describe('terms', () => { setState={setStateSpy} columnId="col1" layerId="first" + currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> @@ -452,6 +453,7 @@ describe('terms', () => { setState={setStateSpy} columnId="col1" layerId="first" + currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> @@ -494,6 +496,7 @@ describe('terms', () => { setState={setStateSpy} columnId="col1" layerId="first" + currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> @@ -510,6 +513,7 @@ describe('terms', () => { setState={setStateSpy} columnId="col1" layerId="first" + currentColumn={state.layers.first.columns.col1 as TermsIndexPatternColumn} storage={{} as Storage} uiSettings={{} as UiSettingsClientContract} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx similarity index 84% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx index c6f74902c5ef5..c13479c04c0c5 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx @@ -7,10 +7,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; -import { TermsIndexPatternColumn, IndexPatternColumn } from '../indexpattern'; -import { OperationDefinition } from '../operations'; -import { updateColumnParam } from '../state_helpers'; -import { DataType } from '../../types'; +import { IndexPatternColumn } from '../../indexpattern'; +import { updateColumnParam } from '../../state_helpers'; +import { DataType } from '../../../types'; +import { OperationDefinition } from '.'; +import { FieldBasedIndexPatternColumn } from './column_types'; type PropType = C extends React.ComponentType ? P : unknown; @@ -37,28 +38,28 @@ function isSortableByColumn(column: IndexPatternColumn) { const DEFAULT_SIZE = 3; +export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'terms'; + params: { + size: number; + orderBy: { type: 'alphabetical' } | { type: 'column'; columnId: string }; + orderDirection: 'asc' | 'desc'; + }; +} + export const termsOperation: OperationDefinition = { type: 'terms', displayName: i18n.translate('xpack.lens.indexPattern.terms', { defaultMessage: 'Top Values', }), - getPossibleOperationsForDocument: () => [], - getPossibleOperationsForField: ({ aggregationRestrictions, aggregatable, type }) => { + getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( (type === 'string' || type === 'boolean') && aggregatable && (!aggregationRestrictions || aggregationRestrictions.terms) ) { - return [ - { - dataType: type, - isBucketed: true, - isMetric: false, - scale: 'ordinal', - }, - ]; + return { dataType: type, isBucketed: true, isMetric: false, scale: 'ordinal' }; } - return []; }, isTransferable: (column, newIndexPattern) => { const newField = newIndexPattern.fields.find(field => field.name === column.sourceField); @@ -71,9 +72,6 @@ export const termsOperation: OperationDefinition = { ); }, buildColumn({ suggestedPriority, columns, field }) { - if (!field) { - throw new Error('Invariant error: terms operation requires field'); - } const existingMetricColumn = Object.entries(columns) .filter(([_columnId, column]) => column && isSortableByColumn(column)) .map(([id]) => id)[0]; @@ -137,8 +135,7 @@ export const termsOperation: OperationDefinition = { } return currentColumn; }, - paramEditor: ({ state, setState, columnId: currentColumnId, layerId }) => { - const currentColumn = state.layers[layerId].columns[currentColumnId] as TermsIndexPatternColumn; + paramEditor: ({ state, setState, currentColumn, columnId: currentColumnId, layerId }) => { const SEPARATOR = '$$$'; function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { if (orderBy.type === 'alphabetical') { @@ -187,7 +184,13 @@ export const termsOperation: OperationDefinition = { showInput onChange={(e: React.ChangeEvent) => setState( - updateColumnParam(state, layerId, currentColumn, 'size', Number(e.target.value)) + updateColumnParam({ + state, + layerId, + currentColumn, + paramName: 'size', + value: Number(e.target.value), + }) ) } aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', { @@ -206,13 +209,13 @@ export const termsOperation: OperationDefinition = { value={toValue(currentColumn.params.orderBy)} onChange={(e: React.ChangeEvent) => setState( - updateColumnParam( + updateColumnParam({ state, layerId, currentColumn, - 'orderBy', - fromValue(e.target.value) - ) + paramName: 'orderBy', + value: fromValue(e.target.value), + }) ) } aria-label={i18n.translate('xpack.lens.indexPattern.terms.orderBy', { @@ -244,8 +247,13 @@ export const termsOperation: OperationDefinition = { value={currentColumn.params.orderDirection} onChange={(e: React.ChangeEvent) => setState( - updateColumnParam(state, layerId, currentColumn, 'orderDirection', e.target - .value as 'asc' | 'desc') + updateColumnParam({ + state, + layerId, + currentColumn, + paramName: 'orderDirection', + value: e.target.value as 'asc' | 'desc', + }) ) } aria-label={i18n.translate('xpack.lens.indexPattern.terms.orderBy', { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts new file mode 100644 index 0000000000000..1e2bc5dcb6b62 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './operations'; +export { OperationType, IndexPatternColumn } from './definitions'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts index 49719c35a09c2..15a3b8ab19296 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts @@ -4,20 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getOperationTypesForField, - getAvailableOperationsByMetadata, - buildColumn, -} from './operations'; -import { - IndexPatternPrivateState, - AvgIndexPatternColumn, - MinIndexPatternColumn, - CountIndexPatternColumn, -} from './indexpattern'; +import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColumn } from '.'; +import { IndexPatternPrivateState } from '../indexpattern'; +import { AvgIndexPatternColumn, MinIndexPatternColumn } from './definitions/metrics'; +import { CountIndexPatternColumn } from './definitions/count'; jest.mock('ui/new_platform'); -jest.mock('./loader'); +jest.mock('../loader'); const expectedIndexPatterns = { 1: { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts new file mode 100644 index 0000000000000..2c9d8a6a8d9eb --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { DimensionPriority, OperationMetadata } from '../../types'; +import { IndexPatternField, IndexPattern } from '../indexpattern'; +import { + operationDefinitionMap, + operationDefinitions, + GenericOperationDefinition, + OperationType, + IndexPatternColumn, +} from './definitions'; + +/** + * Returns all available operation types as a list at runtime. + * This will be an array of each member of the union type `OperationType` + * without any guaranteed order + */ +export function getOperations(): OperationType[] { + return Object.keys(operationDefinitionMap) as OperationType[]; +} + +/** + * Returns true if the given column can be applied to the given index pattern + */ +export function isColumnTransferable(column: IndexPatternColumn, newIndexPattern: IndexPattern) { + return operationDefinitionMap[column.operationType].isTransferable(column, newIndexPattern); +} + +/** + * Returns a list of the display names of all operations with any guaranteed order. + */ +export function getOperationDisplay() { + const display = {} as Record< + OperationType, + { + type: OperationType; + displayName: string; + } + >; + operationDefinitions.forEach(({ type, displayName }) => { + display[type] = { + type, + displayName, + }; + }); + return display; +} + +/** + * Returns all `OperationType`s that can build a column using `buildColumn` based on the + * passed in field. + */ +export function getOperationTypesForField(field: IndexPatternField) { + return operationDefinitions + .filter( + operationDefinition => + 'getPossibleOperationForField' in operationDefinition && + operationDefinition.getPossibleOperationForField(field) + ) + .map(({ type }) => type); +} + +type OperationFieldTuple = + | { type: 'field'; operationType: OperationType; field: string } + | { type: 'document'; operationType: OperationType }; + +/** + * Returns all possible operations (matches between operations and fields of the index + * pattern plus matches for operations and documents of the index pattern) indexed by the + * meta data of the operation. + * + * The resulting list is filtered down by the `filterOperations` function passed in by + * the current visualization to determine which operations and field are applicable for + * a given dimension. + * + * Example output: + * ``` + * [ + * { + * operationMetaData: { dataType: 'string', isBucketed: true }, + * operations: ['terms'] + * }, + * { + * operationMetaData: { dataType: 'number', isBucketed: false }, + * operations: ['avg', 'min', 'max'] + * }, + * ] + * ``` + */ +export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { + const operationByMetadata: Record< + string, + { operationMetaData: OperationMetadata; operations: OperationFieldTuple[] } + > = {}; + + const addToMap = ( + operation: OperationFieldTuple, + operationMetadata: OperationMetadata | undefined | false + ) => { + if (!operationMetadata) return; + const key = JSON.stringify(operationMetadata); + + if (operationByMetadata[key]) { + operationByMetadata[key].operations.push(operation); + } else { + operationByMetadata[key] = { + operationMetaData: operationMetadata, + operations: [operation], + }; + } + }; + + operationDefinitions.forEach(operationDefinition => { + addToMap( + { type: 'document', operationType: operationDefinition.type }, + getPossibleOperationForDocument(operationDefinition, indexPattern) + ); + + indexPattern.fields.forEach(field => { + addToMap( + { + type: 'field', + operationType: operationDefinition.type, + field: field.name, + }, + getPossibleOperationForField(operationDefinition, field) + ); + }); + }); + + return Object.values(operationByMetadata); +} + +function getPossibleOperationForDocument( + operationDefinition: GenericOperationDefinition, + indexPattern: IndexPattern +): OperationMetadata | undefined { + return 'getPossibleOperationForDocument' in operationDefinition + ? operationDefinition.getPossibleOperationForDocument(indexPattern) + : undefined; +} + +function getPossibleOperationForField( + operationDefinition: GenericOperationDefinition, + field: IndexPatternField +): OperationMetadata | undefined { + return 'getPossibleOperationForField' in operationDefinition + ? operationDefinition.getPossibleOperationForField(field) + : undefined; +} + +/** + * Changes the field of the passed in colum. To do so, this method uses the `onFieldChange` function of + * the operation definition of the column. Returns a new column object with the field changed. + * @param column The column object with the old field configured + * @param indexPattern The index pattern associated to the layer of the column + * @param newField The new field the column should be switched to + */ +export function changeField( + column: IndexPatternColumn, + indexPattern: IndexPattern, + newField: IndexPatternField +) { + const operationDefinition = operationDefinitionMap[column.operationType]; + + if (!('onFieldChange' in operationDefinition)) { + throw new Error( + "Invariant error: Cannot change field if operation isn't a field based operaiton" + ); + } + + return operationDefinition.onFieldChange(column, indexPattern, newField); +} + +/** + * Builds a column object based on the context passed in. It tries + * to find the applicable operation definition and then calls the `buildColumn` + * function of that definition. It passes in the given `field` (if available), + * `suggestedPriority`, `layerId` and the currently existing `columns`. + * * If `op` is specified, the specified operation definition is used directly. + * * If `asDocumentOperation` is true, the first matching document-operation is used. + * * If `field` is specified, the first matching field based operation applicable to the field is used. + */ +export function buildColumn({ + op, + columns, + field, + layerId, + indexPattern, + suggestedPriority, + asDocumentOperation, +}: { + op?: OperationType; + columns: Partial>; + suggestedPriority: DimensionPriority | undefined; + layerId: string; + indexPattern: IndexPattern; + field?: IndexPatternField; + asDocumentOperation?: boolean; +}): IndexPatternColumn { + let operationDefinition: GenericOperationDefinition | undefined; + + if (op) { + operationDefinition = operationDefinitionMap[op]; + } else if (asDocumentOperation) { + operationDefinition = operationDefinitions.find(definition => + getPossibleOperationForDocument(definition, indexPattern) + ); + } else if (field) { + operationDefinition = operationDefinitions.find(definition => + getPossibleOperationForField(definition, field) + ); + } + + if (!operationDefinition) { + throw new Error('No suitable operation found for given parameters'); + } + + const baseOptions = { + columns, + suggestedPriority, + layerId, + indexPattern, + }; + + // check for the operation for field getter to determine whether + // this is a field based operation type + if ('getPossibleOperationForField' in operationDefinition) { + if (!field) { + throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); + } + return operationDefinition.buildColumn({ + ...baseOptions, + field, + }); + } else { + return operationDefinition.buildColumn(baseOptions); + } +} + +export { operationDefinitionMap } from './definitions'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts index 897af8bbc28ff..1d366b931b89b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -11,15 +11,11 @@ import { deleteColumn, updateLayerIndexPattern, } from './state_helpers'; -import { - IndexPatternPrivateState, - DateHistogramIndexPatternColumn, - TermsIndexPatternColumn, - AvgIndexPatternColumn, - IndexPattern, - IndexPatternLayer, -} from './indexpattern'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from './indexpattern'; import { operationDefinitionMap } from './operations'; +import { TermsIndexPatternColumn } from './operations/definitions/terms'; +import { DateHistogramIndexPatternColumn } from './operations/definitions/date_histogram'; +import { AvgIndexPatternColumn } from './operations/definitions/metrics'; jest.mock('ui/new_platform'); jest.mock('./operations'); @@ -156,7 +152,13 @@ describe('state_helpers', () => { }; expect( - updateColumnParam(state, 'first', currentColumn, 'interval', 'M').layers.first.columns.col1 + updateColumnParam({ + state, + layerId: 'first', + currentColumn, + paramName: 'interval', + value: 'M', + }).layers.first.columns.col1 ).toEqual({ ...currentColumn, params: { interval: 'M' }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts index 4a17f7774664b..f2b55bbcb0dd5 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -5,25 +5,26 @@ */ import _ from 'lodash'; -import { - IndexPatternPrivateState, - IndexPatternColumn, - BaseIndexPatternColumn, - IndexPatternLayer, - IndexPattern, -} from './indexpattern'; -import { operationDefinitionMap, OperationDefinition, isColumnTransferable } from './operations'; +import { IndexPatternPrivateState, IndexPatternLayer, IndexPattern } from './indexpattern'; +import { isColumnTransferable } from './operations'; +import { operationDefinitionMap, IndexPatternColumn } from './operations'; export function updateColumnParam< - C extends BaseIndexPatternColumn & { params: object }, + C extends IndexPatternColumn & { params: object }, K extends keyof C['params'] ->( - state: IndexPatternPrivateState, - layerId: string, - currentColumn: C, - paramName: K, - value: C['params'][K] -): IndexPatternPrivateState { +>({ + state, + layerId, + currentColumn, + paramName, + value, +}: { + state: IndexPatternPrivateState; + layerId: string; + currentColumn: C; + paramName: K; + value: C['params'][K]; +}): IndexPatternPrivateState { const columnId = Object.entries(state.layers[layerId].columns).find( ([_columnId, column]) => column === currentColumn )![0]; @@ -40,13 +41,13 @@ export function updateColumnParam< ...state.layers[layerId], columns: { ...state.layers[layerId].columns, - [columnId]: ({ + [columnId]: { ...currentColumn, params: { ...currentColumn.params, [paramName]: value, }, - } as unknown) as IndexPatternColumn, + }, }, }, }, @@ -60,19 +61,17 @@ function adjustColumnReferencesForChangedColumn( const newColumns = { ...columns }; Object.keys(newColumns).forEach(currentColumnId => { if (currentColumnId !== columnId) { - const currentColumn = newColumns[currentColumnId] as BaseIndexPatternColumn; - const operationDefinition = operationDefinitionMap[ - currentColumn.operationType - ] as OperationDefinition; - newColumns[currentColumnId] = (operationDefinition.onOtherColumnChanged + const currentColumn = newColumns[currentColumnId]; + const operationDefinition = operationDefinitionMap[currentColumn.operationType]; + newColumns[currentColumnId] = operationDefinition.onOtherColumnChanged ? operationDefinition.onOtherColumnChanged(currentColumn, newColumns) - : currentColumn) as IndexPatternColumn; + : currentColumn; } }); return newColumns; } -export function changeColumn({ +export function changeColumn({ state, layerId, columnId, @@ -82,7 +81,7 @@ export function changeColumn({ state: IndexPatternPrivateState; layerId: string; columnId: string; - newColumn: IndexPatternColumn; + newColumn: C; keepParams?: boolean; }): IndexPatternPrivateState { const oldColumn = state.layers[layerId].columns[columnId]; @@ -92,7 +91,7 @@ export function changeColumn({ oldColumn && oldColumn.operationType === newColumn.operationType && 'params' in oldColumn - ? ({ ...newColumn, params: oldColumn.params } as IndexPatternColumn) + ? { ...newColumn, params: oldColumn.params } : newColumn; const newColumns = adjustColumnReferencesForChangedColumn( @@ -175,9 +174,7 @@ export function updateLayerIndexPattern( isColumnTransferable(column, newIndexPattern) ); const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, column => { - const operationDefinition = operationDefinitionMap[column.operationType] as OperationDefinition< - IndexPatternColumn - >; + const operationDefinition = operationDefinitionMap[column.operationType]; return operationDefinition.transfer ? operationDefinition.transfer(column, newIndexPattern) : column; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index ab06f94117163..9bd68aac90403 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { IndexPatternPrivateState, IndexPatternColumn, IndexPattern } from './indexpattern'; -import { operationDefinitionMap, OperationDefinition, buildColumn } from './operations'; +import { buildColumn, operationDefinitionMap } from './operations'; function getExpressionForLayer( indexPattern: IndexPattern, @@ -20,17 +20,10 @@ function getExpressionForLayer( } function getEsAggsConfig(column: C, columnId: string) { - // Typescript is not smart enough to infer that definitionMap[C['operationType']] is always OperationDefinition, - // but this is made sure by the typing of the operation map - const operationDefinition = (operationDefinitionMap[ - column.operationType - ] as unknown) as OperationDefinition; - return operationDefinition.toEsAggsConfig(column, columnId); + return operationDefinitionMap[column.operationType].toEsAggsConfig(column, columnId); } - const columnEntries = columnOrder.map( - colId => [colId, columns[colId]] as [string, IndexPatternColumn] - ); + const columnEntries = columnOrder.map(colId => [colId, columns[colId]] as const); if (columnEntries.length) { const aggs = columnEntries.map(([colId, col]) => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts index 4871b10cbd99c..aab991a27856a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts @@ -5,7 +5,11 @@ */ import _ from 'lodash'; -import { BaseIndexPatternColumn, FieldBasedIndexPatternColumn, DraggedField } from './indexpattern'; +import { DraggedField } from './indexpattern'; +import { + BaseIndexPatternColumn, + FieldBasedIndexPatternColumn, +} from './operations/definitions/column_types'; export function hasField(column: BaseIndexPatternColumn): column is FieldBasedIndexPatternColumn { return 'sourceField' in column;