diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 5c360a41e337..33567b20f364 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -822,7 +822,7 @@ describe('IndexPatternDimensionPanel', () => { .find(EuiSideNav) .prop('items')[0] .items.map(({ name }) => name) - ).toEqual(['Average', 'Count', 'Filter Ratio', 'Maximum', 'Minimum', 'Sum']); + ).toEqual(['Unique count', 'Average', 'Count', 'Filter Ratio', 'Maximum', 'Minimum', 'Sum']); }); it('should add a column on selection of a field', () => { @@ -956,6 +956,12 @@ describe('IndexPatternDimensionPanel', () => { searchable: true, type: 'number', }, + { + aggregatable: true, + name: 'mystring', + searchable: true, + type: 'string', + }, ], }, }, @@ -1026,7 +1032,7 @@ describe('IndexPatternDimensionPanel', () => { ...dragDropContext, dragging: { indexPatternId: 'foo', - field: { type: 'number', name: 'bar', aggregatable: true }, + field: { type: 'string', name: 'mystring', aggregatable: true }, }, }} state={dragDropState()} @@ -1141,6 +1147,54 @@ describe('IndexPatternDimensionPanel', () => { }); }); + it('selects the specific operation that was valid on drop', () => { + const dragging = { + field: { type: 'string', name: 'mystring', aggregatable: true }, + indexPatternId: 'foo', + }; + const testState = dragDropState(); + wrapper = shallow( + op.isBucketed} + layerId="myLayer" + /> + ); + + act(() => { + const onDrop = wrapper + .find('[data-test-subj="indexPattern-dropTarget"]') + .first() + .prop('onDrop') as DropHandler; + + onDrop(dragging); + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col2'], + columns: { + ...testState.layers.myLayer.columns, + col2: expect.objectContaining({ + dataType: 'string', + sourceField: 'mystring', + }), + }, + }, + }, + }); + }); + it('updates a column when a field is dropped', () => { const dragging = { field: { type: 'number', name: 'bar', aggregatable: true }, 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 e46dc4cc3e00..2f8f46bb2b7e 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 @@ -134,6 +134,7 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan const newColumn = hasFieldChanged ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) : buildColumn({ + op: operationsForNewField ? operationsForNewField[0] : undefined, columns: props.state.layers[props.layerId].columns, indexPattern: currentIndexPattern, layerId, 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 250b43646539..ec2c153931bd 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 @@ -288,6 +288,11 @@ export function PopoverEditor(props: PopoverEditorProps) { column = changeField(selectedColumn, currentIndexPattern, fieldMap[choice.field]); } else { // Otherwise we'll use the buildColumn method to calculate a new column + const compatibleOperations = + ('field' in choice && + operationFieldSupportMatrix.operationByField[choice.field]) || + []; + column = buildColumn({ columns: props.state.layers[props.layerId].columns, field: 'field' in choice ? fieldMap[choice.field] : undefined, @@ -296,7 +301,8 @@ export function PopoverEditor(props: PopoverEditorProps) { suggestedPriority: props.suggestedPriority, op: incompatibleSelectedOperationType || - ('field' in choice ? choice.operationType : undefined), + ('field' in choice ? choice.operationType : undefined) || + compatibleOperations[0], asDocumentOperation: choice.type === 'document', }); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx new file mode 100644 index 000000000000..454169b24440 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx @@ -0,0 +1,79 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { OperationDefinition } from '.'; +import { FieldBasedIndexPatternColumn } from './column_types'; + +const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); + +const SCALE = 'ratio'; +const OPERATION_TYPE = 'cardinality'; +const IS_BUCKETED = false; + +function ofName(name: string) { + return i18n.translate('xpack.lens.indexPattern.cardinalityOf', { + defaultMessage: 'Unique count of {name}', + values: { name }, + }); +} + +export interface CardinalityIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'cardinality'; +} + +export const cardinalityOperation: OperationDefinition = { + type: OPERATION_TYPE, + displayName: i18n.translate('xpack.lens.indexPattern.cardinality', { + defaultMessage: 'Unique count', + }), + getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { + if ( + supportedTypes.has(type) && + aggregatable && + (!aggregationRestrictions || aggregationRestrictions.cardinality) + ) { + return { dataType: 'number', isBucketed: IS_BUCKETED, scale: SCALE }; + } + }, + isTransferable: (column, newIndexPattern) => { + const newField = newIndexPattern.fields.find(field => field.name === column.sourceField); + + return Boolean( + newField && + supportedTypes.has(newField.type) && + newField.aggregatable && + (!newField.aggregationRestrictions || newField.aggregationRestrictions.cardinality) + ); + }, + buildColumn({ suggestedPriority, field }) { + return { + label: ofName(field.name), + dataType: 'number', + operationType: OPERATION_TYPE, + scale: SCALE, + suggestedPriority, + sourceField: field.name, + isBucketed: IS_BUCKETED, + }; + }, + toEsAggsConfig: (column, columnId) => ({ + id: columnId, + enabled: true, + type: OPERATION_TYPE, + schema: 'metric', + params: { + field: column.sourceField, + }, + }), + onFieldChange: (oldColumn, indexPattern, field) => { + return { + ...oldColumn, + label: ofName(field.name), + sourceField: field.name, + }; + }, +}; 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 index 93d9dd68d1c6..4ea734caacd9 100644 --- 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 @@ -11,6 +11,7 @@ import { HttpServiceBase, } from 'src/core/public'; import { termsOperation } from './terms'; +import { cardinalityOperation } from './cardinality'; import { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; import { dateHistogramOperation } from './date_histogram'; import { countOperation } from './count'; @@ -28,6 +29,7 @@ const internalOperationDefinitions = [ minOperation, maxOperation, averageOperation, + cardinalityOperation, sumOperation, countOperation, filterRatioOperation, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts index 0a8e4b57521f..736a6f712d34 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts @@ -274,6 +274,21 @@ describe('getOperationTypesForField', () => { "operationType": "avg", "type": "field", }, + Object { + "field": "timestamp", + "operationType": "cardinality", + "type": "field", + }, + Object { + "field": "bytes", + "operationType": "cardinality", + "type": "field", + }, + Object { + "field": "source", + "operationType": "cardinality", + "type": "field", + }, Object { "field": "bytes", "operationType": "sum",