[Lens] Use entire layers, not specific columns (#82550) (#83088)

* [Lens] Use entire layers, not specific columns

* Fix types

* Move all of state_helpers over

* Fix tests

* Fix crash and add tests to prevent future issues

* Prevent users from dropping duplicate fields

* Respond to review feedback

* Fix review feedback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Wylie Conlon 2020-11-10 17:25:58 -05:00 committed by GitHub
parent ffd8c94689
commit afae7fb618
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1711 additions and 1447 deletions

View file

@ -371,6 +371,43 @@ describe('LayerPanel', () => {
);
});
it('should determine if the datasource supports dropping of a field onto a pre-filled dimension', () => {
mockVisualization.getConfiguration.mockReturnValue({
groups: [
{
groupLabel: 'A',
groupId: 'a',
accessors: ['a'],
filterOperations: () => true,
supportsMoreColumns: true,
dataTestSubj: 'lnsGroup',
},
],
});
mockDatasource.canHandleDrop.mockImplementation(({ columnId }) => columnId !== 'a');
const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' };
const component = mountWithIntl(
<ChildDragDropProvider dragging={draggingField} setDragging={jest.fn()}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>
);
expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith(
expect.objectContaining({ columnId: 'a' })
);
expect(
component.find('DragDrop[data-test-subj="lnsGroup"]').first().prop('droppable')
).toEqual(false);
component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop');
expect(mockDatasource.onDrop).not.toHaveBeenCalled();
});
it('should allow drag to move between groups', () => {
(generateId as jest.Mock).mockReturnValue(`newid`);

View file

@ -235,6 +235,17 @@ export function LayerPanel(
dragging.groupId === group.groupId &&
dragging.columnId !== accessor &&
dragging.groupId !== 'y'; // TODO: remove this line when https://github.com/elastic/elastic-charts/issues/868 is fixed
const isDroppable = isDraggedOperation(dragging)
? dragType === 'reorder'
? isFromTheSameGroup
: isFromCompatibleGroup
: layerDatasource.canHandleDrop({
...layerDatasourceDropProps,
columnId: accessor,
filterOperations: group.filterOperations,
});
return (
<DragDrop
key={accessor}
@ -252,11 +263,7 @@ export function LayerPanel(
}}
isValueEqual={isSameConfiguration}
label={columnLabelMap[accessor]}
droppable={
(dragging && !isDraggedOperation(dragging)) ||
isFromCompatibleGroup ||
isFromTheSameGroup
}
droppable={dragging && isDroppable}
dropTo={(dropTargetId: string) => {
layerDatasource.onDrop({
isReorder: true,
@ -303,7 +310,6 @@ export function LayerPanel(
...layerDatasourceConfigProps,
columnId: accessor,
filterOperations: group.filterOperations,
suggestedPriority: group.suggestedPriority,
onClick: () => {
if (activeId) {
setActiveDimension(initialActiveDimensionState);
@ -450,7 +456,6 @@ export function LayerPanel(
core: props.core,
columnId: activeId,
filterOperations: activeGroup.filterOperations,
suggestedPriority: activeGroup?.suggestedPriority,
dimensionGroups: groups,
setState: (newState: unknown) => {
props.updateAll(

View file

@ -1,21 +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.
*/
const actual = jest.requireActual('../state_helpers');
jest.spyOn(actual, 'changeColumn');
jest.spyOn(actual, 'updateLayerIndexPattern');
export const {
getColumnOrder,
changeColumn,
deleteColumn,
updateColumnParam,
sortByField,
hasField,
updateLayerIndexPattern,
mergeLayer,
} = actual;

View file

@ -31,7 +31,6 @@ describe('BucketNestingEditor', () => {
orderDirection: 'asc',
},
sourceField: 'a',
suggestedPriority: 0,
...col,
};
@ -46,9 +45,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['a', 'b', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1 }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
a: mockCol(),
b: mockCol(),
c: mockCol({ operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
@ -67,9 +66,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['b', 'a', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1 }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
a: mockCol(),
b: mockCol(),
c: mockCol({ operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
@ -89,9 +88,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['b', 'a', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1 }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
a: mockCol(),
b: mockCol(),
c: mockCol({ operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
@ -109,9 +108,9 @@ describe('BucketNestingEditor', () => {
layer: {
columnOrder: ['a', 'b', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1 }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
a: mockCol(),
b: mockCol(),
c: mockCol({ operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
},
@ -134,9 +133,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['a', 'b', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'avg', isBucketed: false }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: false }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
a: mockCol({ operationType: 'avg', isBucketed: false }),
b: mockCol({ operationType: 'max', isBucketed: false }),
c: mockCol({ operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
@ -155,9 +154,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['a', 'b', 'c'],
columns: {
a: mockCol({ suggestedPriority: 0 }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: false }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }),
a: mockCol(),
b: mockCol({ operationType: 'max', isBucketed: false }),
c: mockCol({ operationType: 'min', isBucketed: false }),
},
indexPatternId: 'foo',
}}
@ -176,9 +175,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'count', isBucketed: true }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: true }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: true }),
a: mockCol({ operationType: 'count', isBucketed: true }),
b: mockCol({ operationType: 'max', isBucketed: true }),
c: mockCol({ operationType: 'min', isBucketed: true }),
},
indexPatternId: 'foo',
}}
@ -200,9 +199,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'count', isBucketed: true }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: true }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: true }),
a: mockCol({ operationType: 'count', isBucketed: true }),
b: mockCol({ operationType: 'max', isBucketed: true }),
c: mockCol({ operationType: 'min', isBucketed: true }),
},
indexPatternId: 'foo',
}}
@ -227,9 +226,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'count', isBucketed: true }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: true }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: true }),
a: mockCol({ operationType: 'count', isBucketed: true }),
b: mockCol({ operationType: 'max', isBucketed: true }),
c: mockCol({ operationType: 'min', isBucketed: true }),
},
indexPatternId: 'foo',
}}
@ -254,9 +253,9 @@ describe('BucketNestingEditor', () => {
layer={{
columnOrder: ['c', 'a', 'b'],
columns: {
a: mockCol({ suggestedPriority: 0, operationType: 'count', isBucketed: true }),
b: mockCol({ suggestedPriority: 1, operationType: 'max', isBucketed: true }),
c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: true }),
a: mockCol({ operationType: 'count', isBucketed: true }),
b: mockCol({ operationType: 'max', isBucketed: true }),
c: mockCol({ operationType: 'min', isBucketed: true }),
},
indexPatternId: 'foo',
}}

View file

@ -22,14 +22,16 @@ import { IndexPatternColumn, OperationType } from '../indexpattern';
import {
operationDefinitionMap,
getOperationDisplay,
buildColumn,
changeField,
insertOrReplaceColumn,
replaceColumn,
deleteColumn,
updateColumnParam,
} from '../operations';
import { deleteColumn, changeColumn, updateColumnParam, mergeLayer } from '../state_helpers';
import { mergeLayer } from '../state_helpers';
import { FieldSelect } from './field_select';
import { hasField, fieldIsInvalid } from '../utils';
import { BucketNestingEditor } from './bucket_nesting_editor';
import { IndexPattern } from '../types';
import { IndexPattern, IndexPatternLayer } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { FormatSelector } from './format_selector';
@ -170,21 +172,13 @@ export function DimensionEditor(props: DimensionEditorProps) {
if (selectedColumn?.operationType === operationType) {
return;
}
setState(
changeColumn({
state,
layerId,
columnId,
newColumn: buildColumn({
columns: props.state.layers[props.layerId].columns,
suggestedPriority: props.suggestedPriority,
layerId: props.layerId,
op: operationType,
indexPattern: currentIndexPattern,
previousColumn: selectedColumn,
}),
})
);
const newLayer = insertOrReplaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
columnId,
op: operationType,
});
setState(mergeLayer({ state, layerId, newLayer }));
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
return;
} else if (!selectedColumn || !compatibleWithCurrentField) {
@ -192,18 +186,15 @@ export function DimensionEditor(props: DimensionEditorProps) {
if (possibleFields.size === 1) {
setState(
changeColumn({
mergeLayer({
state,
layerId,
columnId,
newColumn: buildColumn({
columns: props.state.layers[props.layerId].columns,
suggestedPriority: props.suggestedPriority,
layerId: props.layerId,
op: operationType,
newLayer: insertOrReplaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
columnId,
op: operationType,
field: currentIndexPattern.getFieldByName(possibleFields.values().next().value),
previousColumn: selectedColumn,
}),
})
);
@ -216,30 +207,21 @@ export function DimensionEditor(props: DimensionEditorProps) {
setInvalidOperationType(null);
if (selectedColumn?.operationType === operationType) {
if (selectedColumn.operationType === operationType) {
return;
}
const newColumn: IndexPatternColumn = buildColumn({
columns: props.state.layers[props.layerId].columns,
suggestedPriority: props.suggestedPriority,
layerId: props.layerId,
op: operationType,
const newLayer = replaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
columnId,
op: operationType,
field: hasField(selectedColumn)
? currentIndexPattern.getFieldByName(selectedColumn.sourceField)
: undefined,
previousColumn: selectedColumn,
});
setState(
changeColumn({
state,
layerId,
columnId,
newColumn,
})
);
setState(mergeLayer({ state, layerId, newLayer }));
},
};
}
@ -297,30 +279,31 @@ export function DimensionEditor(props: DimensionEditorProps) {
incompatibleSelectedOperationType={incompatibleSelectedOperationType}
onDeleteColumn={() => {
setState(
deleteColumn({
mergeLayer({
state,
layerId,
columnId,
newLayer: deleteColumn({ layer: state.layers[layerId], columnId }),
})
);
}}
onChoose={(choice) => {
let column: IndexPatternColumn;
let newLayer: IndexPatternLayer;
if (
!incompatibleSelectedOperationType &&
selectedColumn &&
'field' in choice &&
choice.operationType === selectedColumn.operationType
) {
// 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,
currentIndexPattern.getFieldByName(choice.field)!
);
// Replaces just the field
newLayer = replaceColumn({
layer: state.layers[layerId],
columnId,
indexPattern: currentIndexPattern,
op: choice.operationType,
field: currentIndexPattern.getFieldByName(choice.field)!,
});
} else {
// Otherwise we'll use the buildColumn method to calculate a new column
// Finds a new operation
const compatibleOperations =
('field' in choice && operationSupportMatrix.operationByField[choice.field]) ||
new Set();
@ -334,26 +317,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
} else if ('field' in choice) {
operation = choice.operationType;
}
column = buildColumn({
columns: props.state.layers[props.layerId].columns,
newLayer = insertOrReplaceColumn({
layer: state.layers[layerId],
columnId,
field: currentIndexPattern.getFieldByName(choice.field),
indexPattern: currentIndexPattern,
layerId: props.layerId,
suggestedPriority: props.suggestedPriority,
op: operation as OperationType,
previousColumn: selectedColumn,
});
}
setState(
changeColumn({
state,
layerId,
columnId,
newColumn: column,
keepParams: false,
})
);
setState(mergeLayer({ state, layerId, newLayer }));
setInvalidOperationType(null);
}}
/>

View file

@ -9,7 +9,6 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { EuiComboBox, EuiListGroupItemProps, EuiListGroup, EuiRange } from '@elastic/eui';
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
import { changeColumn } from '../state_helpers';
import {
IndexPatternDimensionEditorComponent,
IndexPatternDimensionEditorProps,
@ -18,14 +17,14 @@ import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/e
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { IndexPatternPrivateState } from '../types';
import { IndexPatternColumn } from '../operations';
import { IndexPatternColumn, replaceColumn } 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');
jest.mock('../operations');
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');
@ -682,7 +681,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
// Other parts of this don't matter for this test
}),
},
columnOrder: ['col1', 'col2'],
columnOrder: ['col2', 'col1'],
},
},
});
@ -1029,15 +1028,13 @@ describe('IndexPatternDimensionEditorPanel', () => {
);
});
expect(changeColumn).toHaveBeenCalledWith({
state: initialState,
columnId: 'col1',
layerId: 'first',
newColumn: expect.objectContaining({
sourceField: 'bytes',
operationType: 'min',
}),
});
expect(replaceColumn).toHaveBeenCalledWith(
expect.objectContaining({
columnId: 'col1',
op: 'min',
field: expect.objectContaining({ name: 'bytes' }),
})
);
});
it('should clear the dimension when removing the selection in field combobox', () => {

View file

@ -17,7 +17,7 @@ import { OperationMetadata } from '../../types';
import { IndexPatternColumn } from '../operations';
import { getFieldByNameFactory } from '../pure_helpers';
jest.mock('../state_helpers');
jest.mock('../operations');
const fields = [
{
@ -56,8 +56,8 @@ const fields = [
];
const expectedIndexPatterns = {
1: {
id: '1',
foo: {
id: 'foo',
title: 'my-fake-index-pattern',
timeFieldName: 'timestamp',
hasExistence: true,
@ -89,7 +89,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
state = {
indexPatternRefs: [],
indexPatterns: expectedIndexPatterns,
currentIndexPatternId: '1',
currentIndexPatternId: 'foo',
isFirstExistenceFetch: false,
existingFields: {
'my-fake-index-pattern': {
@ -101,7 +101,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
},
layers: {
first: {
indexPatternId: '1',
indexPatternId: 'foo',
columnOrder: ['col1'],
columns: {
col1: {
@ -156,84 +156,8 @@ describe('IndexPatternDimensionEditorPanel', () => {
jest.clearAllMocks();
});
function dragDropState(): IndexPatternPrivateState {
return {
indexPatternRefs: [],
existingFields: {},
indexPatterns: {
foo: {
id: 'foo',
title: 'Foo pattern',
hasRestrictions: false,
fields: [
{
aggregatable: true,
name: 'bar',
displayName: 'bar',
searchable: true,
type: 'number',
},
{
aggregatable: true,
name: 'mystring',
displayName: 'mystring',
searchable: true,
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',
isFirstExistenceFetch: false,
layers: {
myLayer: {
indexPatternId: 'foo',
columnOrder: ['col1'],
columns: {
col1: {
label: 'Date histogram of timestamp',
dataType: 'date',
isBucketed: true,
// Private
operationType: 'date_histogram',
params: {
interval: '1d',
},
sourceField: 'timestamp',
},
},
},
},
};
}
it('is not droppable if no drag is happening', () => {
expect(
canHandleDrop({
...defaultProps,
dragDropContext,
state: dragDropState(),
layerId: 'myLayer',
})
).toBe(false);
expect(canHandleDrop({ ...defaultProps, dragDropContext })).toBe(false);
});
it('is not droppable if the dragged item has no field', () => {
@ -260,9 +184,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
id: 'mystring',
},
},
state: dragDropState(),
filterOperations: () => false,
layerId: 'myLayer',
})
).toBe(false);
});
@ -274,14 +196,12 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragDropContext: {
...dragDropContext,
dragging: {
field: { type: 'number', name: 'bar', aggregatable: true },
field: { type: 'number', name: 'bytes', aggregatable: true },
indexPatternId: 'foo',
id: 'bar',
},
},
state: dragDropState(),
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
layerId: 'myLayer',
})
).toBe(true);
});
@ -298,9 +218,30 @@ describe('IndexPatternDimensionEditorPanel', () => {
id: 'bar',
},
},
state: dragDropState(),
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
layerId: 'myLayer',
})
).toBe(false);
});
it('is not droppable if the dragged field is already in use by this operation', () => {
expect(
canHandleDrop({
...defaultProps,
dragDropContext: {
...dragDropContext,
dragging: {
field: {
name: 'timestamp',
displayName: 'timestampLabel',
type: 'date',
aggregatable: true,
searchable: true,
exists: true,
},
indexPatternId: 'foo',
id: 'bar',
},
},
})
).toBe(false);
});
@ -314,14 +255,11 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragging: {
columnId: 'col1',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'col1',
},
},
state: dragDropState(),
columnId: 'col2',
filterOperations: (op: OperationMetadata) => true,
layerId: 'myLayer',
})
).toBe(true);
});
@ -335,14 +273,10 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragging: {
columnId: 'col1',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'bar',
},
},
state: dragDropState(),
columnId: 'col1',
filterOperations: (op: OperationMetadata) => true,
layerId: 'myLayer',
})
).toBe(false);
});
@ -356,25 +290,22 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragging: {
columnId: 'col1',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'bar',
},
},
state: dragDropState(),
columnId: 'col2',
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
layerId: 'myLayer',
})
).toBe(false);
});
it('appends the dropped column when a field is dropped', () => {
const dragging = {
field: { type: 'number', name: 'bar', aggregatable: true },
field: { type: 'number', name: 'bytes', aggregatable: true },
indexPatternId: 'foo',
id: 'bar',
};
const testState = dragDropState();
onDrop({
...defaultProps,
@ -383,24 +314,22 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragging,
},
droppedItem: dragging,
state: testState,
columnId: 'col2',
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
layerId: 'myLayer',
});
expect(setState).toBeCalledTimes(1);
expect(setState).toHaveBeenCalledWith({
...testState,
...state,
layers: {
myLayer: {
...testState.layers.myLayer,
first: {
...state.layers.first,
columnOrder: ['col1', 'col2'],
columns: {
...testState.layers.myLayer.columns,
...state.layers.first.columns,
col2: expect.objectContaining({
dataType: 'number',
sourceField: 'bar',
sourceField: 'bytes',
}),
},
},
@ -410,11 +339,10 @@ describe('IndexPatternDimensionEditorPanel', () => {
it('selects the specific operation that was valid on drop', () => {
const dragging = {
field: { type: 'string', name: 'mystring', aggregatable: true },
field: { type: 'string', name: 'source', aggregatable: true },
indexPatternId: 'foo',
id: 'bar',
};
const testState = dragDropState();
onDrop({
...defaultProps,
dragDropContext: {
@ -422,24 +350,22 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragging,
},
droppedItem: dragging,
state: testState,
columnId: 'col2',
filterOperations: (op: OperationMetadata) => op.isBucketed,
layerId: 'myLayer',
});
expect(setState).toBeCalledTimes(1);
expect(setState).toHaveBeenCalledWith({
...testState,
...state,
layers: {
myLayer: {
...testState.layers.myLayer,
columnOrder: ['col1', 'col2'],
first: {
...state.layers.first,
columnOrder: ['col2', 'col1'],
columns: {
...testState.layers.myLayer.columns,
...state.layers.first.columns,
col2: expect.objectContaining({
dataType: 'string',
sourceField: 'mystring',
sourceField: 'source',
}),
},
},
@ -449,11 +375,10 @@ describe('IndexPatternDimensionEditorPanel', () => {
it('updates a column when a field is dropped', () => {
const dragging = {
field: { type: 'number', name: 'bar', aggregatable: true },
field: { type: 'number', name: 'bytes', aggregatable: true },
indexPatternId: 'foo',
id: 'bar',
};
const testState = dragDropState();
onDrop({
...defaultProps,
dragDropContext: {
@ -461,20 +386,18 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragging,
},
droppedItem: dragging,
state: testState,
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
layerId: 'myLayer',
});
expect(setState).toBeCalledTimes(1);
expect(setState).toHaveBeenCalledWith({
...testState,
...state,
layers: {
myLayer: expect.objectContaining({
first: expect.objectContaining({
columns: expect.objectContaining({
col1: expect.objectContaining({
dataType: 'number',
sourceField: 'bar',
sourceField: 'bytes',
}),
}),
}),
@ -482,13 +405,12 @@ describe('IndexPatternDimensionEditorPanel', () => {
});
});
it('does not set the size of the terms aggregation', () => {
it('keeps the operation when dropping a different compatible field', () => {
const dragging = {
field: { type: 'string', name: 'mystring', aggregatable: true },
field: { name: 'memory', type: 'number', aggregatable: true },
indexPatternId: 'foo',
id: 'bar',
id: '1',
};
const testState = dragDropState();
onDrop({
...defaultProps,
dragDropContext: {
@ -496,27 +418,41 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragging,
},
droppedItem: dragging,
state: testState,
columnId: 'col2',
filterOperations: (op: OperationMetadata) => op.isBucketed,
layerId: 'myLayer',
state: {
...state,
layers: {
first: {
indexPatternId: 'foo',
columnOrder: ['col1'],
columns: {
col1: {
label: 'Sum of bytes',
dataType: 'number',
isBucketed: false,
// Private
operationType: 'sum',
sourceField: 'bytes',
},
},
},
},
},
});
expect(setState).toBeCalledTimes(1);
expect(setState).toHaveBeenCalledWith({
...testState,
...state,
layers: {
myLayer: {
...testState.layers.myLayer,
columnOrder: ['col1', 'col2'],
columns: {
...testState.layers.myLayer.columns,
col2: expect.objectContaining({
operationType: 'terms',
params: expect.objectContaining({ size: 3 }),
first: expect.objectContaining({
columns: expect.objectContaining({
col1: expect.objectContaining({
operationType: 'sum',
dataType: 'number',
sourceField: 'memory',
}),
},
},
}),
}),
},
});
});
@ -525,10 +461,9 @@ describe('IndexPatternDimensionEditorPanel', () => {
const dragging = {
columnId: 'col1',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'bar',
};
const testState = dragDropState();
onDrop({
...defaultProps,
@ -537,21 +472,18 @@ describe('IndexPatternDimensionEditorPanel', () => {
dragging,
},
droppedItem: dragging,
state: testState,
columnId: 'col2',
filterOperations: (op: OperationMetadata) => true,
layerId: 'myLayer',
});
expect(setState).toBeCalledTimes(1);
expect(setState).toHaveBeenCalledWith({
...testState,
...state,
layers: {
myLayer: {
...testState.layers.myLayer,
first: {
...state.layers.first,
columnOrder: ['col2'],
columns: {
col2: testState.layers.myLayer.columns.col1,
col2: state.layers.first.columns.col1,
},
},
},
@ -562,15 +494,15 @@ describe('IndexPatternDimensionEditorPanel', () => {
const dragging = {
columnId: 'col2',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'col2',
};
const testState = dragDropState();
testState.layers.myLayer = {
const testState = { ...state };
testState.layers.first = {
indexPatternId: 'foo',
columnOrder: ['col1', 'col2', 'col3'],
columns: {
col1: testState.layers.myLayer.columns.col1,
col1: testState.layers.first.columns.col1,
col2: {
label: 'Top values of src',
@ -606,21 +538,18 @@ describe('IndexPatternDimensionEditorPanel', () => {
},
droppedItem: dragging,
state: testState,
columnId: 'col1',
filterOperations: (op: OperationMetadata) => true,
layerId: 'myLayer',
});
expect(setState).toBeCalledTimes(1);
expect(setState).toHaveBeenCalledWith({
...testState,
layers: {
myLayer: {
...testState.layers.myLayer,
first: {
...testState.layers.first,
columnOrder: ['col1', 'col3'],
columns: {
col1: testState.layers.myLayer.columns.col2,
col3: testState.layers.myLayer.columns.col3,
col1: testState.layers.first.columns.col2,
col3: testState.layers.first.columns.col3,
},
},
},
@ -631,13 +560,13 @@ describe('IndexPatternDimensionEditorPanel', () => {
const dragging = {
columnId: 'col1',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'col1',
};
const testState = {
...dragDropState(),
...state,
layers: {
myLayer: {
first: {
indexPatternId: 'foo',
columnOrder: ['col1', 'col2', 'col3'],
columns: {
@ -671,18 +600,17 @@ describe('IndexPatternDimensionEditorPanel', () => {
droppedItem: dragging,
state: testState,
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
layerId: 'myLayer',
};
const stateWithColumnOrder = (columnOrder: string[]) => {
return {
...testState,
layers: {
myLayer: {
...testState.layers.myLayer,
first: {
...testState.layers.first,
columnOrder,
columns: {
...testState.layers.myLayer.columns,
...testState.layers.first.columns,
},
},
},
@ -704,7 +632,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
droppedItem: {
columnId: 'col3',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'col3',
},
});
@ -718,7 +646,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
droppedItem: {
columnId: 'col2',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'col2',
},
});
@ -732,7 +660,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
droppedItem: {
columnId: 'col2',
groupId: 'a',
layerId: 'myLayer',
layerId: 'first',
id: 'col2',
},
});

View file

@ -10,9 +10,9 @@ import {
isDraggedOperation,
} from '../../types';
import { IndexPatternColumn } from '../indexpattern';
import { buildColumn, changeField } from '../operations';
import { changeColumn, mergeLayer } from '../state_helpers';
import { isDraggedField, hasField } from '../utils';
import { insertOrReplaceColumn } from '../operations';
import { mergeLayer } from '../state_helpers';
import { hasField, isDraggedField } from '../utils';
import { IndexPatternPrivateState, IndexPatternField } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { getOperationSupportMatrix } from './operation_support';
@ -28,9 +28,12 @@ export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPr
}
if (isDraggedField(dragging)) {
const currentColumn = props.state.layers[props.layerId].columns[props.columnId];
return (
layerIndexPatternId === dragging.indexPatternId &&
Boolean(hasOperationForField(dragging.field))
Boolean(hasOperationForField(dragging.field)) &&
(!currentColumn ||
(hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name))
);
}
@ -126,52 +129,36 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPr
const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name];
const selectedColumn: IndexPatternColumn | null = state.layers[layerId].columns[columnId] || null;
const currentIndexPattern = state.indexPatterns[state.layers[layerId]?.indexPatternId];
// We need to check if dragging in a new field, was just a field change on the same
// index pattern and on the same operations (therefore checking if the new field supports
// our previous operation)
const hasFieldChanged =
selectedColumn &&
hasField(selectedColumn) &&
selectedColumn.sourceField !== droppedItem.field.name &&
operationsForNewField &&
operationsForNewField.has(selectedColumn.operationType);
if (!operationsForNewField || operationsForNewField.size === 0) {
return false;
}
// 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
? changeField(selectedColumn, currentIndexPattern, droppedItem.field)
: buildColumn({
op: operationsForNewField.values().next().value,
columns: state.layers[layerId].columns,
indexPattern: currentIndexPattern,
layerId,
suggestedPriority: props.suggestedPriority,
field: droppedItem.field,
previousColumn: selectedColumn,
});
const layer = state.layers[layerId];
const selectedColumn: IndexPatternColumn | null = layer.columns[columnId] || null;
const currentIndexPattern = state.indexPatterns[layer.indexPatternId];
// Detects if we can change the field only, otherwise change field + operation
const fieldIsCompatibleWithCurrent =
selectedColumn &&
operationSupportMatrix.operationByField[droppedItem.field.name]?.has(
selectedColumn.operationType
);
const newLayer = insertOrReplaceColumn({
layer,
columnId,
indexPattern: currentIndexPattern,
op: fieldIsCompatibleWithCurrent
? selectedColumn.operationType
: operationsForNewField.values().next().value,
field: droppedItem.field,
});
trackUiEvent('drop_onto_dimension');
const hasData = Object.values(state.layers).some(({ columns }) => columns.length);
trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty');
setState(
changeColumn({
state,
layerId,
columnId,
newColumn,
// If the field has changed, the onFieldChange method needs to take care of everything including moving
// over params. If we create a new column above we want changeColumn to move over params.
keepParams: !hasFieldChanged,
})
);
setState(mergeLayer({ state, layerId, newLayer }));
return true;
}

View file

@ -51,13 +51,14 @@ import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public';
import { deleteColumn } from './state_helpers';
import { mergeLayer } from './state_helpers';
import { Datasource, StateSetter } from '../index';
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { deleteColumn } from './operations';
import { FieldBasedIndexPatternColumn } from './operations/definitions/column_types';
import { Dragging } from '../drag_drop/providers';
export { OperationType, IndexPatternColumn } from './operations';
export { OperationType, IndexPatternColumn, deleteColumn } from './operations';
export type DraggedField = Dragging & {
field: IndexPatternField;
@ -159,10 +160,10 @@ export function getIndexPatternDatasource({
},
removeColumn({ prevState, layerId, columnId }) {
return deleteColumn({
return mergeLayer({
state: prevState,
layerId,
columnId,
newLayer: deleteColumn({ layer: prevState.layers[layerId], columnId }),
});
},

View file

@ -12,6 +12,7 @@ import {
getDatasourceSuggestionsFromCurrentState,
getDatasourceSuggestionsForVisualizeField,
} from './indexpattern_suggestions';
import { documentField } from './document_field';
import { getFieldByNameFactory } from './pure_helpers';
jest.mock('./loader');
@ -60,6 +61,7 @@ const fieldsOne = [
aggregatable: true,
searchable: true,
},
documentField,
];
const fieldsTwo = [
@ -116,6 +118,7 @@ const fieldsTwo = [
},
},
},
documentField,
];
const expectedIndexPatterns = {
@ -290,13 +293,13 @@ describe('IndexPattern Data Source suggestions', () => {
state: expect.objectContaining({
layers: {
id1: expect.objectContaining({
columnOrder: ['id2', 'id3'],
columnOrder: ['id3', 'id2'],
columns: {
id2: expect.objectContaining({
id3: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
id3: expect.objectContaining({
id2: expect.objectContaining({
operationType: 'avg',
sourceField: 'bytes',
}),
@ -310,10 +313,10 @@ describe('IndexPattern Data Source suggestions', () => {
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id2',
columnId: 'id3',
}),
expect.objectContaining({
columnId: 'id3',
columnId: 'id2',
}),
],
layerId: 'id1',
@ -510,13 +513,13 @@ describe('IndexPattern Data Source suggestions', () => {
state: expect.objectContaining({
layers: {
previousLayer: expect.objectContaining({
columnOrder: ['id1', 'id2'],
columnOrder: ['id2', 'id1'],
columns: {
id1: expect.objectContaining({
id2: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'timestamp',
}),
id2: expect.objectContaining({
id1: expect.objectContaining({
operationType: 'avg',
sourceField: 'bytes',
}),
@ -530,10 +533,10 @@ describe('IndexPattern Data Source suggestions', () => {
isMultiRow: true,
columns: [
expect.objectContaining({
columnId: 'id1',
columnId: 'id2',
}),
expect.objectContaining({
columnId: 'id2',
columnId: 'id1',
}),
],
layerId: 'previousLayer',
@ -757,9 +760,9 @@ describe('IndexPattern Data Source suggestions', () => {
layers: {
previousLayer: initialState.layers.previousLayer,
currentLayer: expect.objectContaining({
columnOrder: ['id1', 'colb'],
columnOrder: ['cola', 'colb'],
columns: {
id1: expect.objectContaining({
cola: expect.objectContaining({
operationType: 'date_histogram',
sourceField: 'start_date',
}),
@ -867,7 +870,7 @@ describe('IndexPattern Data Source suggestions', () => {
);
});
it('replaces a metric column on a number field if only one other metric is already set', () => {
it('suggests both replacing and adding metric if only one other metric is set', () => {
const initialState = stateWithNonEmptyTables();
const suggestions = getDatasourceSuggestionsForField(initialState, '1', {
name: 'memory',
@ -895,6 +898,26 @@ describe('IndexPattern Data Source suggestions', () => {
}),
})
);
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: expect.objectContaining({
currentLayer: expect.objectContaining({
columnOrder: ['cola', 'colb', 'id1'],
columns: {
cola: initialState.layers.currentLayer.columns.cola,
colb: initialState.layers.currentLayer.columns.colb,
id1: expect.objectContaining({
operationType: 'avg',
sourceField: 'memory',
}),
},
}),
}),
}),
})
);
});
it('adds a metric column on a number field if no other metrics set', () => {
@ -941,7 +964,20 @@ describe('IndexPattern Data Source suggestions', () => {
);
});
it('adds a metric column on a number field if 2 or more other metric', () => {
it('skips duplicates when the field is already in use', () => {
const initialState = stateWithNonEmptyTables();
const suggestions = getDatasourceSuggestionsForField(initialState, '1', {
name: 'bytes',
displayName: 'bytes',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).not.toContain(expect.objectContaining({ changeType: 'extended' }));
});
it('skips duplicates when the document-specific field is already in use', () => {
const initialState = stateWithNonEmptyTables();
const modifiedState: IndexPatternPrivateState = {
...initialState,
@ -951,45 +987,20 @@ describe('IndexPattern Data Source suggestions', () => {
...initialState.layers.currentLayer,
columns: {
...initialState.layers.currentLayer.columns,
colc: {
dataType: 'number',
colb: {
label: 'Count of records',
dataType: 'document',
isBucketed: false,
sourceField: 'dest',
label: 'Unique count of dest',
operationType: 'cardinality',
operationType: 'count',
sourceField: 'Records',
},
},
columnOrder: ['cola', 'colb', 'colc'],
},
},
};
const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', {
name: 'memory',
displayName: 'memory',
type: 'number',
aggregatable: true,
searchable: true,
});
expect(suggestions).toContainEqual(
expect.objectContaining({
state: expect.objectContaining({
layers: {
previousLayer: modifiedState.layers.previousLayer,
currentLayer: expect.objectContaining({
columnOrder: ['cola', 'colb', 'colc', 'id1'],
columns: {
...modifiedState.layers.currentLayer.columns,
id1: expect.objectContaining({
operationType: 'avg',
sourceField: 'memory',
}),
},
}),
},
}),
})
);
const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', documentField);
expect(suggestions).not.toContain(expect.objectContaining({ changeType: 'extended' }));
});
});

View file

@ -10,14 +10,14 @@ import { generateId } from '../id_generator';
import { DatasourceSuggestion, TableChangeType } from '../types';
import { columnToOperation } from './indexpattern';
import {
buildColumn,
insertNewColumn,
replaceColumn,
getMetricOperationTypes,
getOperationTypesForField,
operationDefinitionMap,
IndexPatternColumn,
OperationType,
} from './operations';
import { operationDefinitions } from './operations/definitions';
import { TermsIndexPatternColumn } from './operations/definitions/terms';
import { hasField, hasInvalidReference } from './utils';
import {
IndexPattern,
@ -137,6 +137,7 @@ export function getDatasourceSuggestionsForVisualizeField(
);
}
// TODO: Stop hard-coding the specific operation types
function getBucketOperation(field: IndexPatternField) {
// We allow numeric bucket types in some cases, but it's generally not the right suggestion,
// so we eliminate it here.
@ -160,29 +161,38 @@ function getExistingLayerSuggestionsForField(
const suggestions: IndexPatternSugestion[] = [];
if (usableAsBucketOperation && !fieldInUse) {
suggestions.push(
buildSuggestion({
state,
updatedLayer: addFieldAsBucketOperation(
layer,
layerId,
indexPattern,
field,
usableAsBucketOperation
),
layerId,
changeType: 'extended',
})
);
}
if (!usableAsBucketOperation && operations.length > 0) {
const updatedLayer = addFieldAsMetricOperation(layer, layerId, indexPattern, field);
if (updatedLayer) {
if (
usableAsBucketOperation === 'date_histogram' &&
layer.columnOrder.some((colId) => layer.columns[colId].operationType === 'date_histogram')
) {
const previousDate = layer.columnOrder.find(
(colId) => layer.columns[colId].operationType === 'date_histogram'
)!;
suggestions.push(
buildSuggestion({
state,
updatedLayer,
updatedLayer: replaceColumn({
layer,
indexPattern,
field,
op: usableAsBucketOperation,
columnId: previousDate,
}),
layerId,
changeType: 'initial',
})
);
} else {
suggestions.push(
buildSuggestion({
state,
updatedLayer: insertNewColumn({
layer,
indexPattern,
field,
op: usableAsBucketOperation,
columnId: generateId(),
}),
layerId,
changeType: 'extended',
})
@ -190,6 +200,50 @@ function getExistingLayerSuggestionsForField(
}
}
if (!usableAsBucketOperation && operations.length > 0 && !fieldInUse) {
const [metricOperation] = getMetricOperationTypes(field);
if (metricOperation) {
const layerWithNewMetric = insertNewColumn({
layer,
indexPattern,
field,
columnId: generateId(),
op: metricOperation.type,
});
if (layerWithNewMetric) {
suggestions.push(
buildSuggestion({
state,
layerId,
updatedLayer: layerWithNewMetric,
changeType: 'extended',
})
);
}
const [, metrics] = separateBucketColumns(layer);
if (metrics.length === 1) {
const layerWithReplacedMetric = replaceColumn({
layer,
indexPattern,
field,
columnId: metrics[0],
op: metricOperation.type,
});
if (layerWithReplacedMetric) {
suggestions.push(
buildSuggestion({
state,
layerId,
updatedLayer: layerWithReplacedMetric,
changeType: 'extended',
})
);
}
}
}
}
const metricSuggestion = createMetricSuggestion(indexPattern, layerId, state, field);
if (metricSuggestion) {
suggestions.push(metricSuggestion);
@ -198,100 +252,6 @@ function getExistingLayerSuggestionsForField(
return suggestions;
}
function addFieldAsMetricOperation(
layer: IndexPatternLayer,
layerId: string,
indexPattern: IndexPattern,
field: IndexPatternField
): IndexPatternLayer | undefined {
const newColumn = getMetricColumn(indexPattern, layerId, field);
const addedColumnId = generateId();
const [, metrics] = separateBucketColumns(layer);
// Add metrics if there are 0 or > 1 metric
if (metrics.length !== 1) {
return {
indexPatternId: indexPattern.id,
columns: {
...layer.columns,
[addedColumnId]: newColumn,
},
columnOrder: [...layer.columnOrder, addedColumnId],
};
}
// Replacing old column with new column, keeping the old ID
const newColumns = { ...layer.columns, [metrics[0]]: newColumn };
return {
indexPatternId: indexPattern.id,
columns: newColumns,
columnOrder: layer.columnOrder, // Order is kept by replacing
};
}
function addFieldAsBucketOperation(
layer: IndexPatternLayer,
layerId: string,
indexPattern: IndexPattern,
field: IndexPatternField,
operation: OperationType
): IndexPatternLayer {
const newColumn = buildColumn({
op: operation,
columns: layer.columns,
layerId,
indexPattern,
suggestedPriority: undefined,
field,
});
const [buckets, metrics] = separateBucketColumns(layer);
const newColumnId = generateId();
const updatedColumns = {
...layer.columns,
[newColumnId]: newColumn,
};
if (buckets.length === 0 && operation === 'terms') {
(newColumn as TermsIndexPatternColumn).params.size = 5;
}
const oldDateHistogramIndex = layer.columnOrder.findIndex(
(columnId) => layer.columns[columnId].operationType === 'date_histogram'
);
const oldDateHistogramId =
oldDateHistogramIndex > -1 ? layer.columnOrder[oldDateHistogramIndex] : null;
let updatedColumnOrder: string[] = [];
if (oldDateHistogramId) {
if (operation === 'terms') {
// Insert the new terms bucket above the first date histogram
updatedColumnOrder = [
...buckets.slice(0, oldDateHistogramIndex),
newColumnId,
...buckets.slice(oldDateHistogramIndex, buckets.length),
...metrics,
];
} else if (operation === 'date_histogram') {
// Replace date histogram with new date histogram
delete updatedColumns[oldDateHistogramId];
updatedColumnOrder = layer.columnOrder.map((columnId) =>
columnId !== oldDateHistogramId ? columnId : newColumnId
);
}
} else {
// Insert the new bucket after existing buckets. Users will see the same data
// they already had, with an extra level of detail.
updatedColumnOrder = [...buckets, newColumnId, ...metrics];
}
return {
indexPatternId: indexPattern.id,
columns: updatedColumns,
columnOrder: updatedColumnOrder,
};
}
function getEmptyLayerSuggestionsForField(
state: IndexPatternPrivateState,
layerId: string,
@ -302,9 +262,9 @@ function getEmptyLayerSuggestionsForField(
let newLayer: IndexPatternLayer | undefined;
const bucketOperation = getBucketOperation(field);
if (bucketOperation) {
newLayer = createNewLayerWithBucketAggregation(layerId, indexPattern, field, bucketOperation);
newLayer = createNewLayerWithBucketAggregation(indexPattern, field, bucketOperation);
} else if (indexPattern.timeFieldName && getOperationTypesForField(field).length > 0) {
newLayer = createNewLayerWithMetricAggregation(layerId, indexPattern, field);
newLayer = createNewLayerWithMetricAggregation(indexPattern, field);
}
const newLayerSuggestions = newLayer
@ -324,77 +284,48 @@ function getEmptyLayerSuggestionsForField(
}
function createNewLayerWithBucketAggregation(
layerId: string,
indexPattern: IndexPattern,
field: IndexPatternField,
operation: OperationType
): IndexPatternLayer {
const countColumn = buildColumn({
return insertNewColumn({
op: 'count',
columns: {},
indexPattern,
layerId,
suggestedPriority: undefined,
layer: insertNewColumn({
op: operation,
layer: { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] },
columnId: generateId(),
field,
indexPattern,
}),
columnId: generateId(),
field: documentField,
});
const col1 = generateId();
const col2 = generateId();
// let column know about count column
const column = buildColumn({
layerId,
op: operation,
indexPattern,
columns: {
[col2]: countColumn,
},
field,
suggestedPriority: undefined,
});
if (operation === 'terms') {
(column as TermsIndexPatternColumn).params.size = 5;
}
return {
indexPatternId: indexPattern.id,
columns: {
[col1]: column,
[col2]: countColumn,
},
columnOrder: [col1, col2],
};
}
function createNewLayerWithMetricAggregation(
layerId: string,
indexPattern: IndexPattern,
field: IndexPatternField
): IndexPatternLayer {
): IndexPatternLayer | undefined {
const dateField = indexPattern.getFieldByName(indexPattern.timeFieldName!);
const [metricOperation] = getMetricOperationTypes(field);
if (!metricOperation) {
return;
}
const column = getMetricColumn(indexPattern, layerId, field);
const dateColumn = buildColumn({
return insertNewColumn({
op: 'date_histogram',
columns: {},
suggestedPriority: undefined,
layer: insertNewColumn({
op: metricOperation.type,
layer: { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] },
columnId: generateId(),
field,
indexPattern,
}),
columnId: generateId(),
field: dateField,
indexPattern,
layerId,
});
const col1 = generateId();
const col2 = generateId();
return {
indexPatternId: indexPattern.id,
columns: {
[col1]: dateColumn,
[col2]: column,
},
columnOrder: [col1, col2],
};
}
export function getDatasourceSuggestionsFromCurrentState(
@ -527,57 +458,33 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId
});
}
function getMetricColumn(indexPattern: IndexPattern, layerId: string, field: IndexPatternField) {
const operationDefinitionsMap = _.keyBy(operationDefinitions, 'type');
const [column] = getOperationTypesForField(field)
.map((type) =>
operationDefinitionsMap[type].buildColumn({
field,
indexPattern,
layerId,
columns: {},
suggestedPriority: 0,
})
)
.filter((op) => (op.dataType === 'number' || op.dataType === 'document') && !op.isBucketed);
return column;
}
function createMetricSuggestion(
indexPattern: IndexPattern,
layerId: string,
state: IndexPatternPrivateState,
field: IndexPatternField
) {
const column = getMetricColumn(indexPattern, layerId, field);
const [operation] = getMetricOperationTypes(field);
if (!column) {
if (!operation) {
return;
}
const newId = generateId();
return buildSuggestion({
layerId,
state,
changeType: 'initial',
updatedLayer: {
indexPatternId: indexPattern.id,
columns: {
[newId]:
column.dataType !== 'document'
? column
: buildColumn({
op: 'count',
columns: {},
indexPattern,
layerId,
suggestedPriority: undefined,
field: documentField,
}),
updatedLayer: insertNewColumn({
layer: {
indexPatternId: indexPattern.id,
columns: {},
columnOrder: [],
},
columnOrder: [newId],
},
columnId: generateId(),
op: operation.type,
field: operation.type === 'count' ? documentField : field,
indexPattern,
}),
});
}
@ -591,6 +498,7 @@ function getNestedTitle([outerBucketLabel, innerBucketLabel]: string[]) {
});
}
// Replaces all metrics on the table with a different field-based function
function createAlternativeMetricSuggestions(
indexPattern: IndexPattern,
layerId: string,
@ -598,6 +506,7 @@ function createAlternativeMetricSuggestions(
) {
const layer = state.layers[layerId];
const suggestions: Array<DatasourceSuggestion<IndexPatternPrivateState>> = [];
layer.columnOrder.forEach((columnId) => {
const column = layer.columns[columnId];
if (!hasField(column)) {
@ -607,39 +516,28 @@ function createAlternativeMetricSuggestions(
if (!field) {
return;
}
const alternativeMetricOperations = getOperationTypesForField(field)
.map((op) =>
buildColumn({
op,
columns: layer.columns,
indexPattern,
layerId,
field,
suggestedPriority: undefined,
})
)
.filter(
(fullOperation) =>
fullOperation.operationType !== column.operationType && !fullOperation.isBucketed
);
if (alternativeMetricOperations.length === 0) {
return;
}
const newId = generateId();
const newColumn = alternativeMetricOperations[0];
const updatedLayer = {
indexPatternId: indexPattern.id,
columns: { [newId]: newColumn },
columnOrder: [newId],
};
suggestions.push(
buildSuggestion({
state,
layerId,
updatedLayer,
changeType: 'initial',
})
const possibleOperations = getMetricOperationTypes(field).filter(
({ type }) => type !== column.operationType
);
if (possibleOperations.length) {
const layerWithNewMetric = replaceColumn({
layer,
indexPattern,
field,
columnId,
op: possibleOperations[0].type,
});
if (layerWithNewMetric) {
suggestions.push(
buildSuggestion({
state,
layerId,
updatedLayer: layerWithNewMetric,
changeType: 'initial',
})
);
}
}
});
return suggestions;
}
@ -651,29 +549,17 @@ function createSuggestionWithDefaultDateHistogram(
) {
const layer = state.layers[layerId];
const indexPattern = state.indexPatterns[layer.indexPatternId];
const newId = generateId();
const [buckets, metrics] = separateBucketColumns(layer);
const timeColumn = buildColumn({
layerId,
op: 'date_histogram',
indexPattern,
columns: layer.columns,
field: timeField,
suggestedPriority: undefined,
});
const updatedLayer = {
indexPatternId: layer.indexPatternId,
columns: {
...layer.columns,
[newId]: timeColumn,
},
columnOrder: [...buckets, newId, ...metrics],
};
return buildSuggestion({
state,
layerId,
updatedLayer,
updatedLayer: insertNewColumn({
layer,
indexPattern,
field: timeField,
op: 'date_histogram',
columnId: generateId(),
}),
label: i18n.translate('xpack.lens.indexpattern.suggestions.overTimeLabel', {
defaultMessage: 'Over time',
}),

View file

@ -13,8 +13,6 @@ import { EuiSelectable } from '@elastic/eui';
import { ChangeIndexPattern } from './change_indexpattern';
import { getFieldByNameFactory } from './pure_helpers';
jest.mock('./state_helpers');
interface IndexPatternPickerOption {
label: string;
checked?: 'on' | 'off';

View file

@ -16,7 +16,7 @@ import {
IndexPatternField,
IndexPatternLayer,
} from './types';
import { updateLayerIndexPattern } from './state_helpers';
import { updateLayerIndexPattern } from './operations';
import { DateRange, ExistingFields } from '../../common/types';
import { BASE_API_URL } from '../../common';
import {

View file

@ -4,20 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
const actual = jest.requireActual('../../operations');
const actualOperations = jest.requireActual('../operations');
const actualHelpers = jest.requireActual('../layer_helpers');
jest.spyOn(actual.operationDefinitionMap.date_histogram, 'paramEditor');
jest.spyOn(actual.operationDefinitionMap.terms, 'onOtherColumnChanged');
jest.spyOn(actualOperations.operationDefinitionMap.date_histogram, 'paramEditor');
jest.spyOn(actualOperations.operationDefinitionMap.terms, 'onOtherColumnChanged');
jest.spyOn(actualHelpers, 'insertOrReplaceColumn');
jest.spyOn(actualHelpers, 'insertNewColumn');
jest.spyOn(actualHelpers, 'replaceColumn');
export const {
getAvailableOperationsByMetadata,
buildColumn,
getOperations,
getOperationDisplay,
getOperationTypesForField,
getOperationResultType,
operationDefinitionMap,
operationDefinitions,
} = actualOperations;
export const {
insertOrReplaceColumn,
insertNewColumn,
replaceColumn,
getColumnOrder,
deleteColumn,
updateColumnParam,
sortByField,
hasField,
updateLayerIndexPattern,
mergeLayer,
isColumnTransferable,
changeField,
} = actual;
} = actualHelpers;

View file

@ -52,20 +52,20 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
(!newField.aggregationRestrictions || newField.aggregationRestrictions.cardinality)
);
},
buildColumn({ suggestedPriority, field, previousColumn }) {
buildColumn({ field, previousColumn }) {
return {
label: ofName(field.displayName),
dataType: 'number',
operationType: OPERATION_TYPE,
scale: SCALE,
suggestedPriority,
sourceField: field.name,
isBucketed: IS_BUCKETED,
params:
previousColumn?.dataType === 'number' &&
previousColumn.params &&
'format' in previousColumn.params
? previousColumn.params
'format' in previousColumn.params &&
previousColumn.params.format
? { format: previousColumn.params.format }
: undefined,
};
},
@ -79,7 +79,7 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
missing: 0,
},
}),
onFieldChange: (oldColumn, indexPattern, field) => {
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,
label: ofName(field.displayName),

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Operation, DimensionPriority } from '../../../types';
import { Operation } from '../../../types';
/**
* This is the root type of a column. If you are implementing a new
@ -14,7 +14,6 @@ import { Operation, DimensionPriority } from '../../../types';
export interface BaseIndexPatternColumn extends Operation {
// Private
operationType: string;
suggestedPriority?: DimensionPriority;
customLabel?: boolean;
}

View file

@ -25,7 +25,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
defaultMessage: 'Count',
}),
input: 'field',
onFieldChange: (oldColumn, indexPattern, field) => {
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,
label: field.displayName,
@ -41,20 +41,20 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
};
}
},
buildColumn({ suggestedPriority, field, previousColumn }) {
buildColumn({ field, previousColumn }) {
return {
label: countLabel,
dataType: 'number',
operationType: 'count',
suggestedPriority,
isBucketed: false,
scale: 'ratio',
sourceField: field.name,
params:
previousColumn?.dataType === 'number' &&
previousColumn.params &&
'format' in previousColumn.params
? previousColumn.params
'format' in previousColumn.params &&
previousColumn.params.format
? { format: previousColumn.params.format }
: undefined,
};
},

View file

@ -189,8 +189,6 @@ describe('date_histogram', () => {
it('should create column object with auto interval for primary time field', () => {
const column = dateHistogramOperation.buildColumn({
columns: {},
suggestedPriority: 0,
layerId: 'first',
indexPattern: createMockedIndexPattern(),
field: {
name: 'timestamp',
@ -207,8 +205,6 @@ describe('date_histogram', () => {
it('should create column object with auto interval for non-primary time fields', () => {
const column = dateHistogramOperation.buildColumn({
columns: {},
suggestedPriority: 0,
layerId: 'first',
indexPattern: createMockedIndexPattern(),
field: {
name: 'start_date',
@ -225,8 +221,6 @@ describe('date_histogram', () => {
it('should create column object with restrictions', () => {
const column = dateHistogramOperation.buildColumn({
columns: {},
suggestedPriority: 0,
layerId: 'first',
indexPattern: createMockedIndexPattern(),
field: {
name: 'timestamp',
@ -334,7 +328,7 @@ describe('date_histogram', () => {
const indexPattern = createMockedIndexPattern();
const newDateField = indexPattern.getFieldByName('start_date')!;
const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField);
const column = dateHistogramOperation.onFieldChange(oldColumn, newDateField);
expect(column).toHaveProperty('sourceField', 'start_date');
expect(column).toHaveProperty('params.interval', 'd');
expect(column.label).toContain('start_date');
@ -354,7 +348,7 @@ describe('date_histogram', () => {
const indexPattern = createMockedIndexPattern();
const newDateField = indexPattern.getFieldByName('start_date')!;
const column = dateHistogramOperation.onFieldChange(oldColumn, indexPattern, newDateField);
const column = dateHistogramOperation.onFieldChange(oldColumn, newDateField);
expect(column).toHaveProperty('sourceField', 'start_date');
expect(column).toHaveProperty('params.interval', 'auto');
expect(column.label).toContain('start_date');

View file

@ -19,7 +19,7 @@ import {
EuiTextColor,
EuiSpacer,
} from '@elastic/eui';
import { updateColumnParam } from '../../state_helpers';
import { updateColumnParam } from '../layer_helpers';
import { OperationDefinition } from './index';
import { FieldBasedIndexPatternColumn } from './column_types';
import { IndexPatternAggRestrictions, search } from '../../../../../../../src/plugins/data/public';
@ -59,7 +59,7 @@ export const dateHistogramOperation: OperationDefinition<
};
}
},
buildColumn({ suggestedPriority, field }) {
buildColumn({ field }) {
let interval = autoInterval;
let timeZone: string | undefined;
if (field.aggregationRestrictions && field.aggregationRestrictions.date_histogram) {
@ -70,7 +70,6 @@ export const dateHistogramOperation: OperationDefinition<
label: field.displayName,
dataType: 'date',
operationType: 'date_histogram',
suggestedPriority,
sourceField: field.name,
isBucketed: true,
scale: 'interval',
@ -112,7 +111,7 @@ export const dateHistogramOperation: OperationDefinition<
return column;
},
onFieldChange: (oldColumn, indexPattern, field) => {
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,
label: field.displayName,
@ -168,15 +167,7 @@ export const dateHistogramOperation: OperationDefinition<
const isCalendarInterval = calendarOnlyIntervals.has(newInterval.unit);
const value = `${isCalendarInterval ? '1' : newInterval.value}${newInterval.unit || 'd'}`;
setState(
updateColumnParam({
state,
layerId,
currentColumn,
value,
paramName: 'interval',
})
);
setState(updateColumnParam({ state, layerId, currentColumn, paramName: 'interval', value }));
};
return (

View file

@ -9,7 +9,7 @@ import React, { MouseEventHandler, useState } from 'react';
import { omit } from 'lodash';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui';
import { updateColumnParam } from '../../../state_helpers';
import { updateColumnParam } from '../../layer_helpers';
import { OperationDefinition } from '../index';
import { BaseIndexPatternColumn } from '../column_types';
import { FilterPopover } from './filter_popover';
@ -75,7 +75,7 @@ export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn, 'n
input: 'none',
isTransferable: () => true,
buildColumn({ suggestedPriority, previousColumn }) {
buildColumn({ previousColumn }) {
let params = { filters: [defaultFilter] };
if (previousColumn?.operationType === 'terms') {
params = {
@ -96,7 +96,6 @@ export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn, 'n
dataType: 'string',
operationType: 'filters',
scale: 'ordinal',
suggestedPriority,
isBucketed: true,
params,
};

View file

@ -23,7 +23,7 @@ import {
} from './metrics';
import { dateHistogramOperation, DateHistogramIndexPatternColumn } from './date_histogram';
import { countOperation, CountIndexPatternColumn } from './count';
import { DimensionPriority, StateSetter, OperationMetadata } from '../../../types';
import { StateSetter, OperationMetadata } from '../../../types';
import { BaseIndexPatternColumn } from './column_types';
import { IndexPatternPrivateState, IndexPattern, IndexPatternField } from '../../types';
import { DateRange } from '../../../../common';
@ -138,8 +138,6 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> {
}
interface BaseBuildColumnArgs {
suggestedPriority: DimensionPriority | undefined;
layerId: string;
columns: Partial<Record<string, IndexPatternColumn>>;
indexPattern: IndexPattern;
}
@ -174,8 +172,7 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> {
buildColumn: (
arg: BaseBuildColumnArgs & {
field: IndexPatternField;
// previousColumn?: IndexPatternColumn;
previousColumn?: C;
previousColumn?: IndexPatternColumn;
}
) => C;
/**
@ -191,15 +188,9 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> {
* 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: FieldBasedIndexPatternColumn,
oldColumn: C,
indexPattern: IndexPattern,
field: IndexPatternField
) => C;
onFieldChange: (oldColumn: C, field: IndexPatternField) => C;
}
interface OperationDefinitionMap<C extends BaseIndexPatternColumn> {

View file

@ -52,18 +52,17 @@ function buildMetricOperation<T extends MetricColumn<string>>({
(!newField.aggregationRestrictions || newField.aggregationRestrictions![type])
);
},
buildColumn: ({ suggestedPriority, field, previousColumn }) => ({
buildColumn: ({ field, previousColumn }) => ({
label: ofName(field.displayName),
dataType: 'number',
operationType: type,
suggestedPriority,
sourceField: field.name,
isBucketed: false,
scale: 'ratio',
params:
previousColumn && previousColumn.dataType === 'number' ? previousColumn.params : undefined,
}),
onFieldChange: (oldColumn, indexPattern, field) => {
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,
label: ofName(field.displayName),

View file

@ -12,7 +12,8 @@ import { Range } from '../../../../../../../../src/plugins/expressions/common/ex
import { RangeEditor } from './range_editor';
import { OperationDefinition } from '../index';
import { FieldBasedIndexPatternColumn } from '../column_types';
import { updateColumnParam, changeColumn } from '../../../state_helpers';
import { updateColumnParam } from '../../layer_helpers';
import { mergeLayer } from '../../../state_helpers';
import { supportedFormats } from '../../../format_column';
import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants';
import { IndexPattern, IndexPatternField } from '../../../types';
@ -121,12 +122,11 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
};
}
},
buildColumn({ suggestedPriority, indexPattern, field }) {
buildColumn({ field }) {
return {
label: field.name,
dataType: 'number', // string for Range
operationType: 'range',
suggestedPriority,
sourceField: field.name,
isBucketed: true,
scale: 'interval', // ordinal for Range
@ -149,7 +149,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
(!newField.aggregationRestrictions || newField.aggregationRestrictions.range)
);
},
onFieldChange: (oldColumn, indexPattern, field) => {
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,
label: field.name,
@ -211,23 +211,26 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
? { id: 'range', params: { template: 'arrow_right', replaceInfinity: true } }
: undefined;
setState(
changeColumn({
mergeLayer({
state,
layerId,
columnId,
newColumn: {
...currentColumn,
scale,
dataType,
params: {
type: newMode,
ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }],
maxBars: maxBarsDefaultValue,
format: currentColumn.params.format,
parentFormat,
newLayer: {
columns: {
...state.layers[layerId].columns,
[columnId]: {
...currentColumn,
scale,
dataType,
params: {
type: newMode,
ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }],
maxBars: maxBarsDefaultValue,
format: currentColumn.params.format,
parentFormat,
},
},
},
},
keepParams: false,
})
);
};

View file

@ -8,7 +8,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import { IndexPatternColumn } from '../../../indexpattern';
import { updateColumnParam } from '../../../state_helpers';
import { updateColumnParam } from '../../layer_helpers';
import { DataType } from '../../../../types';
import { OperationDefinition } from '../index';
import { FieldBasedIndexPatternColumn } from '../column_types';
@ -70,21 +70,23 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
(!newField.aggregationRestrictions || newField.aggregationRestrictions.terms)
);
},
buildColumn({ suggestedPriority, columns, field }) {
buildColumn({ columns, field }) {
const existingMetricColumn = Object.entries(columns)
.filter(([_columnId, column]) => column && isSortableByColumn(column))
.map(([id]) => id)[0];
const previousBucketsLength = Object.values(columns).filter((col) => col && col.isBucketed)
.length;
return {
label: ofName(field.displayName),
dataType: field.type as DataType,
operationType: 'terms',
scale: 'ordinal',
suggestedPriority,
sourceField: field.name,
isBucketed: true,
params: {
size: DEFAULT_SIZE,
size: previousBucketsLength === 0 ? 5 : DEFAULT_SIZE,
orderBy: existingMetricColumn
? { type: 'column', columnId: existingMetricColumn }
: { type: 'alphabetical' },
@ -111,7 +113,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
},
};
},
onFieldChange: (oldColumn, indexPattern, field) => {
onFieldChange: (oldColumn, field) => {
const newParams = { ...oldColumn.params };
if ('format' in newParams && field.type !== 'number') {
delete newParams.format;

View file

@ -105,7 +105,7 @@ describe('terms', () => {
const indexPattern = createMockedIndexPattern();
const newNumberField = indexPattern.getFieldByName('bytes')!;
const column = termsOperation.onFieldChange(oldColumn, indexPattern, newNumberField);
const column = termsOperation.onFieldChange(oldColumn, newNumberField);
expect(column).toHaveProperty('dataType', 'number');
expect(column).toHaveProperty('sourceField', 'bytes');
expect(column).toHaveProperty('params.size', 5);
@ -133,7 +133,7 @@ describe('terms', () => {
const indexPattern = createMockedIndexPattern();
const newStringField = indexPattern.fields.find((i) => i.name === 'source')!;
const column = termsOperation.onFieldChange(oldColumn, indexPattern, newStringField);
const column = termsOperation.onFieldChange(oldColumn, newStringField);
expect(column).toHaveProperty('dataType', 'string');
expect(column).toHaveProperty('sourceField', 'source');
expect(column.params.format).toBeUndefined();
@ -236,8 +236,6 @@ describe('terms', () => {
describe('buildColumn', () => {
it('should use type from the passed field', () => {
const termsColumn = termsOperation.buildColumn({
layerId: 'first',
suggestedPriority: undefined,
indexPattern: createMockedIndexPattern(),
field: {
aggregatable: true,
@ -253,8 +251,6 @@ describe('terms', () => {
it('should use existing metric column as order column', () => {
const termsColumn = termsOperation.buildColumn({
layerId: 'first',
suggestedPriority: undefined,
indexPattern: createMockedIndexPattern(),
columns: {
col1: {
@ -279,6 +275,36 @@ describe('terms', () => {
})
);
});
it('should use the default size when there is an existing bucket', () => {
const termsColumn = termsOperation.buildColumn({
indexPattern: createMockedIndexPattern(),
columns: state.layers.first.columns,
field: {
aggregatable: true,
searchable: true,
type: 'boolean',
name: 'test',
displayName: 'test',
},
});
expect(termsColumn.params).toEqual(expect.objectContaining({ size: 3 }));
});
it('should use a size of 5 when there are no other buckets', () => {
const termsColumn = termsOperation.buildColumn({
indexPattern: createMockedIndexPattern(),
columns: {},
field: {
aggregatable: true,
searchable: true,
type: 'boolean',
name: 'test',
displayName: 'test',
},
});
expect(termsColumn.params).toEqual(expect.objectContaining({ size: 5 }));
});
});
describe('onOtherColumnChanged', () => {

View file

@ -5,4 +5,5 @@
*/
export * from './operations';
export * from './layer_helpers';
export { OperationType, IndexPatternColumn, FieldBasedIndexPatternColumn } from './definitions';

View file

@ -0,0 +1,344 @@
/*
* 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 _, { partition } from 'lodash';
import {
operationDefinitionMap,
operationDefinitions,
OperationType,
IndexPatternColumn,
} from './definitions';
import {
IndexPattern,
IndexPatternField,
IndexPatternLayer,
IndexPatternPrivateState,
} from '../types';
import { getSortScoreByPriority } from './operations';
import { mergeLayer } from '../state_helpers';
interface ColumnChange {
op: OperationType;
layer: IndexPatternLayer;
columnId: string;
indexPattern: IndexPattern;
field?: IndexPatternField;
}
export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer {
if (args.layer.columns[args.columnId]) {
return replaceColumn(args);
}
return insertNewColumn(args);
}
export function insertNewColumn({
op,
layer,
columnId,
field,
indexPattern,
}: ColumnChange): IndexPatternLayer {
const operationDefinition = operationDefinitionMap[op];
if (!operationDefinition) {
throw new Error('No suitable operation found for given parameters');
}
const baseOptions = {
columns: layer.columns,
indexPattern,
previousColumn: layer.columns[columnId],
};
// TODO: Reference based operations require more setup to create the references
if (operationDefinition.input === 'none') {
const possibleOperation = operationDefinition.getPossibleOperation();
if (!possibleOperation) {
throw new Error('Tried to create an invalid operation');
}
const isBucketed = Boolean(possibleOperation.isBucketed);
if (isBucketed) {
return addBucket(layer, operationDefinition.buildColumn(baseOptions), columnId);
} else {
return addMetric(layer, operationDefinition.buildColumn(baseOptions), columnId);
}
}
if (!field) {
throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`);
}
const possibleOperation = operationDefinition.getPossibleOperationForField(field);
if (!possibleOperation) {
throw new Error(
`Tried to create an invalid operation ${operationDefinition.type} on ${field.name}`
);
}
const isBucketed = Boolean(possibleOperation.isBucketed);
if (isBucketed) {
return addBucket(layer, operationDefinition.buildColumn({ ...baseOptions, field }), columnId);
} else {
return addMetric(layer, operationDefinition.buildColumn({ ...baseOptions, field }), columnId);
}
}
export function replaceColumn({
layer,
columnId,
indexPattern,
op,
field,
}: ColumnChange): IndexPatternLayer {
const previousColumn = layer.columns[columnId];
if (!previousColumn) {
throw new Error(`Can't replace column because there is no prior column`);
}
const isNewOperation = Boolean(op) && op !== previousColumn.operationType;
const operationDefinition = operationDefinitionMap[op || previousColumn.operationType];
if (!operationDefinition) {
throw new Error('No suitable operation found for given parameters');
}
const baseOptions = {
columns: layer.columns,
indexPattern,
previousColumn,
};
if (isNewOperation) {
// TODO: Reference based operations require more setup to create the references
if (operationDefinition.input === 'none') {
const newColumn = operationDefinition.buildColumn(baseOptions);
if (previousColumn.customLabel) {
newColumn.customLabel = true;
newColumn.label = previousColumn.label;
}
return {
...layer,
columns: adjustColumnReferencesForChangedColumn(
{ ...layer.columns, [columnId]: newColumn },
columnId
),
};
}
if (!field) {
throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`);
}
const newColumn = operationDefinition.buildColumn({ ...baseOptions, field });
if (previousColumn.customLabel) {
newColumn.customLabel = true;
newColumn.label = previousColumn.label;
}
const newColumns = { ...layer.columns, [columnId]: newColumn };
return {
...layer,
columnOrder: getColumnOrder({ ...layer, columns: newColumns }),
columns: adjustColumnReferencesForChangedColumn(newColumns, columnId),
};
} else if (
operationDefinition.input === 'field' &&
field &&
'sourceField' in previousColumn &&
previousColumn.sourceField !== field.name
) {
// Same operation, new field
const newColumn = operationDefinition.onFieldChange(previousColumn, field);
if (previousColumn.customLabel) {
newColumn.customLabel = true;
newColumn.label = previousColumn.label;
}
const newColumns = { ...layer.columns, [columnId]: newColumn };
return {
...layer,
columnOrder: getColumnOrder({ ...layer, columns: newColumns }),
columns: adjustColumnReferencesForChangedColumn(newColumns, columnId),
};
} else {
throw new Error('nothing changed');
}
}
function addBucket(
layer: IndexPatternLayer,
column: IndexPatternColumn,
addedColumnId: string
): IndexPatternLayer {
const [buckets, metrics] = separateBucketColumns(layer);
const oldDateHistogramIndex = layer.columnOrder.findIndex(
(columnId) => layer.columns[columnId].operationType === 'date_histogram'
);
let updatedColumnOrder: string[] = [];
if (oldDateHistogramIndex > -1 && column.operationType === 'terms') {
// Insert the new terms bucket above the first date histogram
updatedColumnOrder = [
...buckets.slice(0, oldDateHistogramIndex),
addedColumnId,
...buckets.slice(oldDateHistogramIndex, buckets.length),
...metrics,
];
} else {
// Insert the new bucket after existing buckets. Users will see the same data
// they already had, with an extra level of detail.
updatedColumnOrder = [...buckets, addedColumnId, ...metrics];
}
return {
...layer,
columns: { ...layer.columns, [addedColumnId]: column },
columnOrder: updatedColumnOrder,
};
}
function addMetric(
layer: IndexPatternLayer,
column: IndexPatternColumn,
addedColumnId: string
): IndexPatternLayer {
return {
...layer,
columns: {
...layer.columns,
[addedColumnId]: column,
},
columnOrder: [...layer.columnOrder, addedColumnId],
};
}
function separateBucketColumns(layer: IndexPatternLayer) {
return partition(layer.columnOrder, (columnId) => layer.columns[columnId]?.isBucketed);
}
export function getMetricOperationTypes(field: IndexPatternField) {
return operationDefinitions.sort(getSortScoreByPriority).filter((definition) => {
if (definition.input !== 'field') return;
const metadata = definition.getPossibleOperationForField(field);
return metadata && !metadata.isBucketed && metadata.dataType === 'number';
});
}
export function updateColumnParam<C extends IndexPatternColumn>({
state,
layerId,
currentColumn,
paramName,
value,
}: {
state: IndexPatternPrivateState;
layerId: string;
currentColumn: C;
paramName: string;
value: unknown;
}): IndexPatternPrivateState {
const columnId = Object.entries(state.layers[layerId].columns).find(
([_columnId, column]) => column === currentColumn
)![0];
const layer = state.layers[layerId];
return mergeLayer({
state,
layerId,
newLayer: {
columns: {
...layer.columns,
[columnId]: {
...currentColumn,
params: {
...currentColumn.params,
[paramName]: value,
},
},
},
},
});
}
function adjustColumnReferencesForChangedColumn(
columns: Record<string, IndexPatternColumn>,
columnId: string
) {
const newColumns = { ...columns };
Object.keys(newColumns).forEach((currentColumnId) => {
if (currentColumnId !== columnId) {
const currentColumn = newColumns[currentColumnId];
const operationDefinition = operationDefinitionMap[currentColumn.operationType];
newColumns[currentColumnId] = operationDefinition.onOtherColumnChanged
? operationDefinition.onOtherColumnChanged(currentColumn, newColumns)
: currentColumn;
}
});
return newColumns;
}
export function deleteColumn({
layer,
columnId,
}: {
layer: IndexPatternLayer;
columnId: string;
}): IndexPatternLayer {
const hypotheticalColumns = { ...layer.columns };
delete hypotheticalColumns[columnId];
const newLayer = {
...layer,
columns: adjustColumnReferencesForChangedColumn(hypotheticalColumns, columnId),
};
return { ...newLayer, columnOrder: getColumnOrder(newLayer) };
}
export function getColumnOrder(layer: IndexPatternLayer): string[] {
const [aggregations, metrics] = _.partition(
Object.entries(layer.columns),
([id, col]) => col.isBucketed
);
return aggregations.map(([id]) => id).concat(metrics.map(([id]) => id));
}
/**
* 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);
}
export function updateLayerIndexPattern(
layer: IndexPatternLayer,
newIndexPattern: IndexPattern
): IndexPatternLayer {
const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) =>
isColumnTransferable(column, newIndexPattern)
);
const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => {
const operationDefinition = operationDefinitionMap[column.operationType];
return operationDefinition.transfer
? operationDefinition.transfer(column, newIndexPattern)
: column;
});
const newColumnOrder = layer.columnOrder.filter((columnId) => newColumns[columnId]);
return {
...layer,
indexPatternId: newIndexPattern.id,
columns: newColumns,
columnOrder: newColumnOrder,
};
}

View file

@ -4,10 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColumn } from './index';
import { AvgIndexPatternColumn } from './definitions/metrics';
import { IndexPatternPrivateState } from '../types';
import { documentField } from '../document_field';
import { getOperationTypesForField, getAvailableOperationsByMetadata } from './index';
import { getFieldByNameFactory } from '../pure_helpers';
jest.mock('../loader');
@ -157,73 +154,6 @@ describe('getOperationTypesForField', () => {
});
});
describe('buildColumn', () => {
const state: IndexPatternPrivateState = {
indexPatternRefs: [],
existingFields: {},
currentIndexPatternId: '1',
isFirstExistenceFetch: false,
indexPatterns: expectedIndexPatterns,
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1'],
columns: {
col1: {
label: 'Date histogram of timestamp',
dataType: 'date',
isBucketed: true,
// Private
operationType: 'date_histogram',
params: {
interval: '1d',
},
sourceField: 'timestamp',
},
},
},
},
};
it('should build a column for the given field-based operation type if it is passed in', () => {
const column = buildColumn({
layerId: 'first',
indexPattern: expectedIndexPatterns[1],
columns: state.layers.first.columns,
suggestedPriority: 0,
op: 'count',
field: documentField,
});
expect(column.operationType).toEqual('count');
});
it('should build a column for the given no-input operation type if it is passed in', () => {
const column = buildColumn({
layerId: 'first',
indexPattern: expectedIndexPatterns[1],
columns: state.layers.first.columns,
suggestedPriority: 0,
op: 'filters',
});
expect(column.operationType).toEqual('filters');
});
it('should build a column for the given operation type and field if it is passed in', () => {
const field = expectedIndexPatterns[1].fields[1];
const column = buildColumn({
layerId: 'first',
indexPattern: expectedIndexPatterns[1],
columns: state.layers.first.columns,
suggestedPriority: 0,
op: 'avg',
field,
}) as AvgIndexPatternColumn;
expect(column.operationType).toEqual('avg');
expect(column.sourceField).toEqual(field.name);
});
});
describe('getAvailableOperationsByMetaData', () => {
it('should put the average operation first', () => {
const numberOperation = getAvailableOperationsByMetadata(expectedIndexPatterns[1]).find(

View file

@ -4,17 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DimensionPriority, OperationMetadata } from '../../types';
import _ from 'lodash';
import { OperationMetadata } from '../../types';
import {
operationDefinitionMap,
operationDefinitions,
GenericOperationDefinition,
OperationType,
IndexPatternColumn,
} from './definitions';
import { IndexPattern, IndexPatternField } from '../types';
import { documentField } from '../document_field';
export { operationDefinitionMap } 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`
@ -24,13 +26,6 @@ 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.
*/
@ -51,7 +46,10 @@ export function getOperationDisplay() {
return display;
}
function getSortScoreByPriority(a: GenericOperationDefinition, b: GenericOperationDefinition) {
export function getSortScoreByPriority(
a: GenericOperationDefinition,
b: GenericOperationDefinition
) {
return (b.priority || Number.NEGATIVE_INFINITY) - (a.priority || Number.NEGATIVE_INFINITY);
}
@ -169,82 +167,3 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) {
return Object.values(operationByMetadata);
}
/**
* 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 (operationDefinition.input === 'field' && 'sourceField' in column) {
return operationDefinition.onFieldChange(column, indexPattern, newField);
} else {
throw new Error(
"Invariant error: Cannot change field if operation isn't a field based operaiton"
);
}
}
/**
* 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,
previousColumn,
}: {
op: OperationType;
columns: Partial<Record<string, IndexPatternColumn>>;
suggestedPriority: DimensionPriority | undefined;
layerId: string;
indexPattern: IndexPattern;
field?: IndexPatternField;
previousColumn?: IndexPatternColumn;
}): IndexPatternColumn {
const operationDefinition = operationDefinitionMap[op];
if (!operationDefinition) {
throw new Error('No suitable operation found for given parameters');
}
const baseOptions = {
columns,
suggestedPriority,
layerId,
indexPattern,
previousColumn,
};
if (operationDefinition.input === 'none') {
return operationDefinition.buildColumn(baseOptions);
}
if (!field) {
throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`);
}
return operationDefinition.buildColumn({
...baseOptions,
field,
});
}
export { operationDefinitionMap } from './definitions';

View file

@ -4,162 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import { isColumnTransferable, operationDefinitionMap, IndexPatternColumn } from './operations';
import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types';
export function updateColumnParam<C extends IndexPatternColumn, K extends keyof C['params']>({
state,
layerId,
currentColumn,
paramName,
value,
}: {
state: IndexPatternPrivateState;
layerId: string;
currentColumn: C;
paramName: string;
value: unknown;
}): IndexPatternPrivateState {
const columnId = Object.entries(state.layers[layerId].columns).find(
([_columnId, column]) => column === currentColumn
)![0];
const layer = state.layers[layerId];
return mergeLayer({
state,
layerId,
newLayer: {
columns: {
...layer.columns,
[columnId]: {
...currentColumn,
params: {
...currentColumn.params,
[paramName]: value,
},
},
},
},
});
}
function adjustColumnReferencesForChangedColumn(
columns: Record<string, IndexPatternColumn>,
columnId: string
) {
const newColumns = { ...columns };
Object.keys(newColumns).forEach((currentColumnId) => {
if (currentColumnId !== columnId) {
const currentColumn = newColumns[currentColumnId];
const operationDefinition = operationDefinitionMap[currentColumn.operationType];
newColumns[currentColumnId] = operationDefinition.onOtherColumnChanged
? operationDefinition.onOtherColumnChanged(currentColumn, newColumns)
: currentColumn;
}
});
return newColumns;
}
export function changeColumn<C extends IndexPatternColumn>({
state,
layerId,
columnId,
newColumn,
keepParams = true,
}: {
state: IndexPatternPrivateState;
layerId: string;
columnId: string;
newColumn: C;
keepParams?: boolean;
}): IndexPatternPrivateState {
const oldColumn = state.layers[layerId].columns[columnId];
const updatedColumn =
keepParams &&
oldColumn &&
oldColumn.operationType === newColumn.operationType &&
'params' in oldColumn
? { ...newColumn, params: oldColumn.params }
: newColumn;
if (oldColumn && oldColumn.customLabel) {
updatedColumn.customLabel = true;
updatedColumn.label = oldColumn.label;
}
const layer = {
...state.layers[layerId],
};
const newColumns = adjustColumnReferencesForChangedColumn(
{
...layer.columns,
[columnId]: updatedColumn,
},
columnId
);
return mergeLayer({
state,
layerId,
newLayer: {
columnOrder: getColumnOrder({
...layer,
columns: newColumns,
}),
columns: newColumns,
},
});
}
export function deleteColumn({
state,
layerId,
columnId,
}: {
state: IndexPatternPrivateState;
layerId: string;
columnId: string;
}): IndexPatternPrivateState {
const hypotheticalColumns = { ...state.layers[layerId].columns };
delete hypotheticalColumns[columnId];
const newColumns = adjustColumnReferencesForChangedColumn(hypotheticalColumns, columnId);
const layer = {
...state.layers[layerId],
columns: newColumns,
};
return mergeLayer({
state,
layerId,
newLayer: {
...layer,
columnOrder: getColumnOrder(layer),
},
});
}
export function getColumnOrder(layer: IndexPatternLayer): string[] {
const [aggregations, metrics] = _.partition(
Object.entries(layer.columns),
([id, col]) => col.isBucketed
);
return aggregations
.sort(([id, col], [id2, col2]) => {
return (
// Sort undefined orders last
(col.suggestedPriority !== undefined ? col.suggestedPriority : Number.MAX_SAFE_INTEGER) -
(col2.suggestedPriority !== undefined ? col2.suggestedPriority : Number.MAX_SAFE_INTEGER)
);
})
.map(([id]) => id)
.concat(metrics.map(([id]) => id));
}
import { IndexPatternPrivateState, IndexPatternLayer } from './types';
export function mergeLayer({
state,
@ -178,26 +23,3 @@ export function mergeLayer({
},
};
}
export function updateLayerIndexPattern(
layer: IndexPatternLayer,
newIndexPattern: IndexPattern
): IndexPatternLayer {
const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) =>
isColumnTransferable(column, newIndexPattern)
);
const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => {
const operationDefinition = operationDefinitionMap[column.operationType];
return operationDefinition.transfer
? operationDefinition.transfer(column, newIndexPattern)
: column;
});
const newColumnOrder = layer.columnOrder.filter((columnId) => newColumns[columnId]);
return {
...layer,
indexPatternId: newIndexPattern.id,
columns: newColumns,
columnOrder: newColumnOrder,
};
}

View file

@ -72,9 +72,6 @@ export interface EditorFrameStart {
createInstance: () => Promise<EditorFrameInstance>;
}
// Hints the default nesting to the data source. 0 is the highest priority
export type DimensionPriority = 0 | 1 | 2;
export interface TableSuggestionColumn {
columnId: string;
operation: Operation;
@ -220,11 +217,6 @@ interface SharedDimensionProps {
*/
filterOperations: (operation: OperationMetadata) => boolean;
/** Visualizations can hint at the role this dimension would play, which
* affects the default ordering of the query
*/
suggestedPriority?: DimensionPriority;
/** Some dimension editors will allow users to change the operation grouping
* from the panel, and this lets the visualization hint that it doesn't want
* users to have that level of control

View file

@ -178,7 +178,6 @@ export const getXyVisualization = ({
groupLabel: getAxisName('x', { isHorizontal }),
accessors: layer.xAccessor ? [layer.xAccessor] : [],
filterOperations: isBucketed,
suggestedPriority: 1,
supportsMoreColumns: !layer.xAccessor,
dataTestSubj: 'lnsXY_xDimensionPanel',
},
@ -199,7 +198,6 @@ export const getXyVisualization = ({
}),
accessors: layer.splitAccessor ? [layer.splitAccessor] : [],
filterOperations: isBucketed,
suggestedPriority: 0,
supportsMoreColumns: !layer.splitAccessor,
dataTestSubj: 'lnsXY_splitDimensionPanel',
required: layer.seriesType.includes('percentage'),

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { migrations } from './migrations';
import { SavedObjectMigrationContext } from 'src/core/server';
import { migrations, LensDocShape } from './migrations';
import { SavedObjectMigrationContext, SavedObjectMigrationFn } from 'src/core/server';
describe('Lens migrations', () => {
describe('7.7.0 missing dimensions in XY', () => {
@ -507,4 +507,88 @@ describe('Lens migrations', () => {
expect(result).toMatchSnapshot();
});
});
describe('7.11.0 remove suggested priority', () => {
const context = ({ log: { warning: () => {} } } as unknown) as SavedObjectMigrationContext;
const example = {
type: 'lens',
attributes: {
state: {
datasourceStates: {
indexpattern: {
currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
layers: {
'bd09dc71-a7e2-42d0-83bd-85df8291f03c': {
indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
columns: {
'1d9cc16c-1460-41de-88f8-471932ecbc97': {
label: 'products.created_on',
dataType: 'date',
operationType: 'date_histogram',
sourceField: 'products.created_on',
isBucketed: true,
scale: 'interval',
params: { interval: 'auto' },
suggestedPriority: 0,
},
'66115819-8481-4917-a6dc-8ffb10dd02df': {
label: 'Count of records',
dataType: 'number',
operationType: 'count',
suggestedPriority: 1,
isBucketed: false,
scale: 'ratio',
sourceField: 'Records',
},
},
columnOrder: [
'1d9cc16c-1460-41de-88f8-471932ecbc97',
'66115819-8481-4917-a6dc-8ffb10dd02df',
],
},
},
},
},
datasourceMetaData: {
filterableIndexPatterns: [
{ id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' },
],
},
visualization: {
legend: { isVisible: true, position: 'right' },
preferredSeriesType: 'bar_stacked',
layers: [
{
layerId: 'bd09dc71-a7e2-42d0-83bd-85df8291f03c',
accessors: ['66115819-8481-4917-a6dc-8ffb10dd02df'],
position: 'top',
seriesType: 'bar_stacked',
showGridlines: false,
xAccessor: '1d9cc16c-1460-41de-88f8-471932ecbc97',
},
],
},
query: { query: '', language: 'kuery' },
filters: [],
},
title: 'Bar chart',
visualizationType: 'lnsXY',
},
};
it('should remove the suggested priority from all columns', () => {
const result = migrations['7.11.0'](example, context) as ReturnType<
SavedObjectMigrationFn<LensDocShape, LensDocShape>
>;
const resultLayers = result.attributes.state.datasourceStates.indexpattern.layers;
const layersWithSuggestedPriority = Object.values(resultLayers).reduce(
(count, layer) =>
count + Object.values(layer.columns).filter((col) => 'suggestedPriority' in col).length,
0
);
expect(layersWithSuggestedPriority).toEqual(0);
});
});
});

View file

@ -31,7 +31,7 @@ interface LensDocShapePre710<VisualizationState = unknown> {
string,
{
columnOrder: string[];
columns: Record<string, unknown>;
columns: Record<string, Record<string, unknown>>;
indexPatternId: string;
}
>;
@ -43,7 +43,7 @@ interface LensDocShapePre710<VisualizationState = unknown> {
};
}
interface LensDocShape<VisualizationState = unknown> {
export interface LensDocShape<VisualizationState = unknown> {
id?: string;
type?: string;
visualizationType: string | null;
@ -56,7 +56,7 @@ interface LensDocShape<VisualizationState = unknown> {
string,
{
columnOrder: string[];
columns: Record<string, unknown>;
columns: Record<string, Record<string, unknown>>;
}
>;
};
@ -310,10 +310,34 @@ const extractReferences: SavedObjectMigrationFn<LensDocShapePre710, LensDocShape
return newDoc;
};
const removeSuggestedPriority: SavedObjectMigrationFn<LensDocShape, LensDocShape> = (doc) => {
const newDoc = cloneDeep(doc);
const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {};
newDoc.attributes.state.datasourceStates.indexpattern.layers = Object.fromEntries(
Object.entries(datasourceLayers).map(([layerId, layer]) => {
return [
layerId,
{
...layer,
columns: Object.fromEntries(
Object.entries(layer.columns).map(([columnId, column]) => {
const copy = { ...column };
delete copy.suggestedPriority;
return [columnId, copy];
})
),
},
];
})
);
return newDoc;
};
export const migrations: SavedObjectMigrationMap = {
'7.7.0': removeInvalidAccessors,
// The order of these migrations matter, since the timefield migration relies on the aggConfigs
// sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was).
'7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context),
'7.10.0': extractReferences,
'7.11.0': removeSuggestedPriority,
};