mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* [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:
parent
ffd8c94689
commit
afae7fb618
35 changed files with 1711 additions and 1447 deletions
|
@ -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`);
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
|
@ -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',
|
||||
}}
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 }),
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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' }));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -5,4 +5,5 @@
|
|||
*/
|
||||
|
||||
export * from './operations';
|
||||
export * from './layer_helpers';
|
||||
export { OperationType, IndexPatternColumn, FieldBasedIndexPatternColumn } from './definitions';
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue