[Lens] Add cardinality operation (#46397) (#46718)

Add cardinality operation to Lens. Still need to figure
out how to pass precision_threshold through
to esaggs, so for now that's not configurable.
This commit is contained in:
Chris Davies 2019-09-26 17:07:56 -04:00 committed by GitHub
parent f582da0f0a
commit ccba346cd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 3 deletions

View file

@ -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(
<IndexPatternDimensionPanel
{...defaultProps}
dragDropContext={{
...dragDropContext,
dragging,
}}
state={testState}
columnId={'col2'}
filterOperations={op => 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 },

View file

@ -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,

View file

@ -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',
});
}

View file

@ -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<CardinalityIndexPatternColumn> = {
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,
};
},
};

View file

@ -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,

View file

@ -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",