[Lens] Split up dimension panel code (#80423)
* [Lens] Split up dimension panel code * Fix test failures * Style updates Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f68e0a36d5
commit
f0023ed879
|
@ -17,4 +17,5 @@ export const {
|
|||
sortByField,
|
||||
hasField,
|
||||
updateLayerIndexPattern,
|
||||
mergeLayer,
|
||||
} = actual;
|
||||
|
|
|
@ -16,7 +16,8 @@ import {
|
|||
EuiListGroupItemProps,
|
||||
EuiFormLabel,
|
||||
} from '@elastic/eui';
|
||||
import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from './dimension_panel';
|
||||
import { IndexPatternDimensionEditorProps } from './dimension_panel';
|
||||
import { OperationSupportMatrix } from './operation_support';
|
||||
import { IndexPatternColumn, OperationType } from '../indexpattern';
|
||||
import {
|
||||
operationDefinitionMap,
|
||||
|
@ -24,7 +25,7 @@ import {
|
|||
buildColumn,
|
||||
changeField,
|
||||
} from '../operations';
|
||||
import { deleteColumn, changeColumn, updateColumnParam } from '../state_helpers';
|
||||
import { deleteColumn, changeColumn, updateColumnParam, mergeLayer } from '../state_helpers';
|
||||
import { FieldSelect } from './field_select';
|
||||
import { hasField, fieldIsInvalid } from '../utils';
|
||||
import { BucketNestingEditor } from './bucket_nesting_editor';
|
||||
|
@ -394,12 +395,11 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
<LabelInput
|
||||
value={selectedColumn.label}
|
||||
onChange={(value) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: {
|
||||
...state.layers[layerId],
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
columns: {
|
||||
...state.layers[layerId].columns,
|
||||
[columnId]: {
|
||||
|
@ -409,8 +409,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,28 +4,19 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLink, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import {
|
||||
DatasourceDimensionTriggerProps,
|
||||
DatasourceDimensionEditorProps,
|
||||
DatasourceDimensionDropProps,
|
||||
DatasourceDimensionDropHandlerProps,
|
||||
isDraggedOperation,
|
||||
} from '../../types';
|
||||
import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types';
|
||||
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
|
||||
import { IndexPatternColumn, OperationType } from '../indexpattern';
|
||||
import { getAvailableOperationsByMetadata, buildColumn, changeField } from '../operations';
|
||||
import { IndexPatternColumn } from '../indexpattern';
|
||||
import { fieldIsInvalid } from '../utils';
|
||||
import { IndexPatternPrivateState } from '../types';
|
||||
import { DimensionEditor } from './dimension_editor';
|
||||
import { changeColumn } from '../state_helpers';
|
||||
import { isDraggedField, hasField, fieldIsInvalid } from '../utils';
|
||||
import { IndexPatternPrivateState, IndexPatternField } from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { DateRange } from '../../../common';
|
||||
import { getOperationSupportMatrix } from './operation_support';
|
||||
|
||||
export type IndexPatternDimensionTriggerProps = DatasourceDimensionTriggerProps<
|
||||
IndexPatternPrivateState
|
||||
|
@ -46,189 +37,6 @@ export type IndexPatternDimensionEditorProps = DatasourceDimensionEditorProps<
|
|||
dateRange: DateRange;
|
||||
};
|
||||
|
||||
export interface OperationSupportMatrix {
|
||||
operationByField: Partial<Record<string, OperationType[]>>;
|
||||
operationWithoutField: OperationType[];
|
||||
fieldByOperation: Partial<Record<OperationType, string[]>>;
|
||||
}
|
||||
|
||||
type Props = Pick<
|
||||
DatasourceDimensionDropProps<IndexPatternPrivateState>,
|
||||
'layerId' | 'columnId' | 'state' | 'filterOperations'
|
||||
>;
|
||||
|
||||
// TODO: This code has historically been memoized, as a potentially performance
|
||||
// sensitive task. If we can add memoization without breaking the behavior, we should.
|
||||
const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => {
|
||||
const layerId = props.layerId;
|
||||
const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId];
|
||||
|
||||
const filteredOperationsByMetadata = getAvailableOperationsByMetadata(
|
||||
currentIndexPattern
|
||||
).filter((operation) => props.filterOperations(operation.operationMetaData));
|
||||
|
||||
const supportedOperationsByField: Partial<Record<string, OperationType[]>> = {};
|
||||
const supportedOperationsWithoutField: OperationType[] = [];
|
||||
const supportedFieldsByOperation: Partial<Record<OperationType, string[]>> = {};
|
||||
|
||||
filteredOperationsByMetadata.forEach(({ operations }) => {
|
||||
operations.forEach((operation) => {
|
||||
if (operation.type === 'field') {
|
||||
if (supportedOperationsByField[operation.field]) {
|
||||
supportedOperationsByField[operation.field]!.push(operation.operationType);
|
||||
} else {
|
||||
supportedOperationsByField[operation.field] = [operation.operationType];
|
||||
}
|
||||
|
||||
if (supportedFieldsByOperation[operation.operationType]) {
|
||||
supportedFieldsByOperation[operation.operationType]!.push(operation.field);
|
||||
} else {
|
||||
supportedFieldsByOperation[operation.operationType] = [operation.field];
|
||||
}
|
||||
} else if (operation.type === 'none') {
|
||||
supportedOperationsWithoutField.push(operation.operationType);
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
operationByField: _.mapValues(supportedOperationsByField, _.uniq),
|
||||
operationWithoutField: _.uniq(supportedOperationsWithoutField),
|
||||
fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq),
|
||||
};
|
||||
};
|
||||
|
||||
export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
|
||||
const { dragging } = props.dragDropContext;
|
||||
const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId;
|
||||
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
}
|
||||
|
||||
if (isDraggedField(dragging)) {
|
||||
return (
|
||||
layerIndexPatternId === dragging.indexPatternId &&
|
||||
Boolean(hasOperationForField(dragging.field))
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDraggedOperation(dragging) &&
|
||||
dragging.layerId === props.layerId &&
|
||||
props.columnId !== dragging.columnId
|
||||
) {
|
||||
const op = props.state.layers[props.layerId].columns[dragging.columnId];
|
||||
return props.filterOperations(op);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
const droppedItem = props.droppedItem;
|
||||
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
}
|
||||
|
||||
if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) {
|
||||
const layer = props.state.layers[props.layerId];
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
if (!props.filterOperations(op)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newColumns = { ...layer.columns };
|
||||
delete newColumns[droppedItem.columnId];
|
||||
newColumns[props.columnId] = op;
|
||||
|
||||
const newColumnOrder = [...layer.columnOrder];
|
||||
const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId);
|
||||
const newIndex = newColumnOrder.findIndex((c) => c === props.columnId);
|
||||
|
||||
if (newIndex === -1) {
|
||||
newColumnOrder[oldIndex] = props.columnId;
|
||||
} else {
|
||||
newColumnOrder.splice(oldIndex, 1);
|
||||
}
|
||||
|
||||
// Time to replace
|
||||
props.setState({
|
||||
...props.state,
|
||||
layers: {
|
||||
...props.state.layers,
|
||||
[props.layerId]: {
|
||||
...layer,
|
||||
columnOrder: newColumnOrder,
|
||||
columns: newColumns,
|
||||
},
|
||||
},
|
||||
});
|
||||
return { deleted: droppedItem.columnId };
|
||||
}
|
||||
|
||||
if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) {
|
||||
// TODO: What do we do if we couldn't find a column?
|
||||
return false;
|
||||
}
|
||||
|
||||
const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name];
|
||||
|
||||
const layerId = props.layerId;
|
||||
const selectedColumn: IndexPatternColumn | null =
|
||||
props.state.layers[layerId].columns[props.columnId] || null;
|
||||
const currentIndexPattern =
|
||||
props.state.indexPatterns[props.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.includes(selectedColumn.operationType);
|
||||
|
||||
if (!operationsForNewField || operationsForNewField.length === 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[0],
|
||||
columns: props.state.layers[props.layerId].columns,
|
||||
indexPattern: currentIndexPattern,
|
||||
layerId,
|
||||
suggestedPriority: props.suggestedPriority,
|
||||
field: droppedItem.field,
|
||||
previousColumn: selectedColumn,
|
||||
});
|
||||
|
||||
trackUiEvent('drop_onto_dimension');
|
||||
const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length);
|
||||
trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty');
|
||||
|
||||
props.setState(
|
||||
changeColumn({
|
||||
state: props.state,
|
||||
layerId,
|
||||
columnId: props.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,
|
||||
})
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function wrapOnDot(str?: string) {
|
||||
// u200B is a non-width white-space character, which allows
|
||||
// the browser to efficiently word-wrap right after the dot
|
||||
|
|
|
@ -0,0 +1,594 @@
|
|||
/*
|
||||
* 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 { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
|
||||
import { IndexPatternDimensionEditorProps } from './dimension_panel';
|
||||
import { onDrop, canHandleDrop } from './droppable';
|
||||
import { DragContextState } from '../../drag_drop';
|
||||
import { createMockedDragDropContext } from '../mocks';
|
||||
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { IndexPatternPrivateState } from '../types';
|
||||
import { documentField } from '../document_field';
|
||||
import { OperationMetadata } from '../../types';
|
||||
|
||||
jest.mock('../state_helpers');
|
||||
|
||||
const expectedIndexPatterns = {
|
||||
1: {
|
||||
id: '1',
|
||||
title: 'my-fake-index-pattern',
|
||||
timeFieldName: 'timestamp',
|
||||
hasExistence: true,
|
||||
hasRestrictions: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'timestamp',
|
||||
displayName: 'timestampLabel',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: 'bytes',
|
||||
displayName: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: 'memory',
|
||||
displayName: 'memory',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: 'source',
|
||||
displayName: 'source',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
exists: true,
|
||||
},
|
||||
documentField,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The datasource exposes four main pieces of code which are tested at
|
||||
* an integration test level. The main reason for this fairly high level
|
||||
* of testing is that there is a lot of UI logic that isn't easily
|
||||
* unit tested, such as the transient invalid state.
|
||||
*
|
||||
* - Dimension trigger: Not tested here
|
||||
* - Dimension editor component: First half of the tests
|
||||
*
|
||||
* - canHandleDrop: Tests for dropping of fields or other dimensions
|
||||
* - onDrop: Correct application of drop logic
|
||||
*/
|
||||
describe('IndexPatternDimensionEditorPanel', () => {
|
||||
let state: IndexPatternPrivateState;
|
||||
let setState: jest.Mock;
|
||||
let defaultProps: IndexPatternDimensionEditorProps;
|
||||
let dragDropContext: DragContextState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
indexPatternRefs: [],
|
||||
indexPatterns: expectedIndexPatterns,
|
||||
currentIndexPatternId: '1',
|
||||
isFirstExistenceFetch: false,
|
||||
existingFields: {
|
||||
'my-fake-index-pattern': {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setState = jest.fn();
|
||||
|
||||
dragDropContext = createMockedDragDropContext();
|
||||
|
||||
defaultProps = {
|
||||
state,
|
||||
setState,
|
||||
dateRange: { fromDate: 'now-1d', toDate: 'now' },
|
||||
columnId: 'col1',
|
||||
layerId: 'first',
|
||||
uniqueLabel: 'stuff',
|
||||
filterOperations: () => true,
|
||||
storage: {} as IStorageWrapper,
|
||||
uiSettings: {} as IUiSettingsClient,
|
||||
savedObjectsClient: {} as SavedObjectsClientContract,
|
||||
http: {} as HttpSetup,
|
||||
data: ({
|
||||
fieldFormats: ({
|
||||
getType: jest.fn().mockReturnValue({
|
||||
id: 'number',
|
||||
title: 'Number',
|
||||
}),
|
||||
getDefaultType: jest.fn().mockReturnValue({
|
||||
id: 'bytes',
|
||||
title: 'Bytes',
|
||||
}),
|
||||
} as unknown) as DataPublicPluginStart['fieldFormats'],
|
||||
} as unknown) as DataPublicPluginStart,
|
||||
core: {} as CoreSetup,
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
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);
|
||||
});
|
||||
|
||||
it('is not droppable if the dragged item has no field', () => {
|
||||
expect(
|
||||
canHandleDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: { name: 'bar' },
|
||||
},
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('is not droppable if field is not supported by filterOperations', () => {
|
||||
expect(
|
||||
canHandleDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
indexPatternId: 'foo',
|
||||
field: { type: 'string', name: 'mystring', aggregatable: true },
|
||||
},
|
||||
},
|
||||
state: dragDropState(),
|
||||
filterOperations: () => false,
|
||||
layerId: 'myLayer',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('is droppable if the field is supported by filterOperations', () => {
|
||||
expect(
|
||||
canHandleDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
field: { type: 'number', name: 'bar', aggregatable: true },
|
||||
indexPatternId: 'foo',
|
||||
},
|
||||
},
|
||||
state: dragDropState(),
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
layerId: 'myLayer',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('is not droppable if the field belongs to another index pattern', () => {
|
||||
expect(
|
||||
canHandleDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
field: { type: 'number', name: 'bar', aggregatable: true },
|
||||
indexPatternId: 'foo2',
|
||||
},
|
||||
},
|
||||
state: dragDropState(),
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
layerId: 'myLayer',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('is droppable if the dragged column is compatible', () => {
|
||||
expect(
|
||||
canHandleDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'a',
|
||||
layerId: 'myLayer',
|
||||
},
|
||||
},
|
||||
state: dragDropState(),
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => true,
|
||||
layerId: 'myLayer',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('is not droppable if the dragged column is the same as the current column', () => {
|
||||
expect(
|
||||
canHandleDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'a',
|
||||
layerId: 'myLayer',
|
||||
},
|
||||
},
|
||||
state: dragDropState(),
|
||||
columnId: 'col1',
|
||||
filterOperations: (op: OperationMetadata) => true,
|
||||
layerId: 'myLayer',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('is not droppable if the dragged column is incompatible', () => {
|
||||
expect(
|
||||
canHandleDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'a',
|
||||
layerId: 'myLayer',
|
||||
},
|
||||
},
|
||||
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 },
|
||||
indexPatternId: 'foo',
|
||||
};
|
||||
const testState = dragDropState();
|
||||
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
droppedItem: dragging,
|
||||
state: testState,
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
layerId: 'myLayer',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
expect(setState).toHaveBeenCalledWith({
|
||||
...testState,
|
||||
layers: {
|
||||
myLayer: {
|
||||
...testState.layers.myLayer,
|
||||
columnOrder: ['col1', 'col2'],
|
||||
columns: {
|
||||
...testState.layers.myLayer.columns,
|
||||
col2: expect.objectContaining({
|
||||
dataType: 'number',
|
||||
sourceField: 'bar',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('selects the specific operation that was valid on drop', () => {
|
||||
const dragging = {
|
||||
field: { type: 'string', name: 'mystring', aggregatable: true },
|
||||
indexPatternId: 'foo',
|
||||
};
|
||||
const testState = dragDropState();
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
droppedItem: dragging,
|
||||
state: testState,
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => op.isBucketed,
|
||||
layerId: 'myLayer',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
expect(setState).toHaveBeenCalledWith({
|
||||
...testState,
|
||||
layers: {
|
||||
myLayer: {
|
||||
...testState.layers.myLayer,
|
||||
columnOrder: ['col1', 'col2'],
|
||||
columns: {
|
||||
...testState.layers.myLayer.columns,
|
||||
col2: expect.objectContaining({
|
||||
dataType: 'string',
|
||||
sourceField: 'mystring',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a column when a field is dropped', () => {
|
||||
const dragging = {
|
||||
field: { type: 'number', name: 'bar', aggregatable: true },
|
||||
indexPatternId: 'foo',
|
||||
};
|
||||
const testState = dragDropState();
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
droppedItem: dragging,
|
||||
state: testState,
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
layerId: 'myLayer',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
expect(setState).toHaveBeenCalledWith({
|
||||
...testState,
|
||||
layers: {
|
||||
myLayer: expect.objectContaining({
|
||||
columns: expect.objectContaining({
|
||||
col1: expect.objectContaining({
|
||||
dataType: 'number',
|
||||
sourceField: 'bar',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not set the size of the terms aggregation', () => {
|
||||
const dragging = {
|
||||
field: { type: 'string', name: 'mystring', aggregatable: true },
|
||||
indexPatternId: 'foo',
|
||||
};
|
||||
const testState = dragDropState();
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
droppedItem: dragging,
|
||||
state: testState,
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => op.isBucketed,
|
||||
layerId: 'myLayer',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
expect(setState).toHaveBeenCalledWith({
|
||||
...testState,
|
||||
layers: {
|
||||
myLayer: {
|
||||
...testState.layers.myLayer,
|
||||
columnOrder: ['col1', 'col2'],
|
||||
columns: {
|
||||
...testState.layers.myLayer.columns,
|
||||
col2: expect.objectContaining({
|
||||
operationType: 'terms',
|
||||
params: expect.objectContaining({ size: 3 }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the column id when moving an operation to an empty dimension', () => {
|
||||
const dragging = {
|
||||
columnId: 'col1',
|
||||
groupId: 'a',
|
||||
layerId: 'myLayer',
|
||||
};
|
||||
const testState = dragDropState();
|
||||
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
droppedItem: dragging,
|
||||
state: testState,
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => true,
|
||||
layerId: 'myLayer',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
expect(setState).toHaveBeenCalledWith({
|
||||
...testState,
|
||||
layers: {
|
||||
myLayer: {
|
||||
...testState.layers.myLayer,
|
||||
columnOrder: ['col2'],
|
||||
columns: {
|
||||
col2: testState.layers.myLayer.columns.col1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces an operation when moving to a populated dimension', () => {
|
||||
const dragging = {
|
||||
columnId: 'col2',
|
||||
groupId: 'a',
|
||||
layerId: 'myLayer',
|
||||
};
|
||||
const testState = dragDropState();
|
||||
testState.layers.myLayer = {
|
||||
indexPatternId: 'foo',
|
||||
columnOrder: ['col1', 'col2', 'col3'],
|
||||
columns: {
|
||||
col1: testState.layers.myLayer.columns.col1,
|
||||
|
||||
col2: {
|
||||
label: 'Top values of src',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
|
||||
// Private
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
orderBy: { type: 'column', columnId: 'col3' },
|
||||
orderDirection: 'desc',
|
||||
size: 10,
|
||||
},
|
||||
sourceField: 'src',
|
||||
},
|
||||
col3: {
|
||||
label: 'Count',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'count',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
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,
|
||||
columnOrder: ['col1', 'col3'],
|
||||
columns: {
|
||||
col1: testState.layers.myLayer.columns.col2,
|
||||
col3: testState.layers.myLayer.columns.col3,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 {
|
||||
DatasourceDimensionDropProps,
|
||||
DatasourceDimensionDropHandlerProps,
|
||||
isDraggedOperation,
|
||||
} from '../../types';
|
||||
import { IndexPatternColumn } from '../indexpattern';
|
||||
import { buildColumn, changeField } from '../operations';
|
||||
import { changeColumn, mergeLayer } from '../state_helpers';
|
||||
import { isDraggedField, hasField } from '../utils';
|
||||
import { IndexPatternPrivateState, IndexPatternField } from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { getOperationSupportMatrix } from './operation_support';
|
||||
|
||||
export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
|
||||
const { dragging } = props.dragDropContext;
|
||||
const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId;
|
||||
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
}
|
||||
|
||||
if (isDraggedField(dragging)) {
|
||||
return (
|
||||
layerIndexPatternId === dragging.indexPatternId &&
|
||||
Boolean(hasOperationForField(dragging.field))
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDraggedOperation(dragging) &&
|
||||
dragging.layerId === props.layerId &&
|
||||
props.columnId !== dragging.columnId
|
||||
) {
|
||||
const op = props.state.layers[props.layerId].columns[dragging.columnId];
|
||||
return props.filterOperations(op);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
const droppedItem = props.droppedItem;
|
||||
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
}
|
||||
|
||||
if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) {
|
||||
const layer = props.state.layers[props.layerId];
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
if (!props.filterOperations(op)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newColumns = { ...layer.columns };
|
||||
delete newColumns[droppedItem.columnId];
|
||||
newColumns[props.columnId] = op;
|
||||
|
||||
const newColumnOrder = [...layer.columnOrder];
|
||||
const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId);
|
||||
const newIndex = newColumnOrder.findIndex((c) => c === props.columnId);
|
||||
|
||||
if (newIndex === -1) {
|
||||
newColumnOrder[oldIndex] = props.columnId;
|
||||
} else {
|
||||
newColumnOrder.splice(oldIndex, 1);
|
||||
}
|
||||
|
||||
// Time to replace
|
||||
props.setState(
|
||||
mergeLayer({
|
||||
state: props.state,
|
||||
layerId: props.layerId,
|
||||
newLayer: {
|
||||
columnOrder: newColumnOrder,
|
||||
columns: newColumns,
|
||||
},
|
||||
})
|
||||
);
|
||||
return { deleted: droppedItem.columnId };
|
||||
}
|
||||
|
||||
if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) {
|
||||
// TODO: What do we do if we couldn't find a column?
|
||||
return false;
|
||||
}
|
||||
|
||||
const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name];
|
||||
|
||||
const layerId = props.layerId;
|
||||
const selectedColumn: IndexPatternColumn | null =
|
||||
props.state.layers[layerId].columns[props.columnId] || null;
|
||||
const currentIndexPattern =
|
||||
props.state.indexPatterns[props.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.includes(selectedColumn.operationType);
|
||||
|
||||
if (!operationsForNewField || operationsForNewField.length === 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[0],
|
||||
columns: props.state.layers[props.layerId].columns,
|
||||
indexPattern: currentIndexPattern,
|
||||
layerId,
|
||||
suggestedPriority: props.suggestedPriority,
|
||||
field: droppedItem.field,
|
||||
previousColumn: selectedColumn,
|
||||
});
|
||||
|
||||
trackUiEvent('drop_onto_dimension');
|
||||
const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length);
|
||||
trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty');
|
||||
|
||||
props.setState(
|
||||
changeColumn({
|
||||
state: props.state,
|
||||
layerId,
|
||||
columnId: props.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,
|
||||
})
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
|
@ -20,7 +20,7 @@ import { EuiHighlight } from '@elastic/eui';
|
|||
import { OperationType } from '../indexpattern';
|
||||
import { LensFieldIcon } from '../lens_field_icon';
|
||||
import { DataType } from '../../types';
|
||||
import { OperationSupportMatrix } from './dimension_panel';
|
||||
import { OperationSupportMatrix } from './operation_support';
|
||||
import { IndexPattern, IndexPatternField, IndexPatternPrivateState } from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { fieldExists } from '../pure_helpers';
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
*/
|
||||
|
||||
export * from './dimension_panel';
|
||||
export * from './droppable';
|
||||
export * from './operation_support';
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { DatasourceDimensionDropProps } from '../../types';
|
||||
import { OperationType } from '../indexpattern';
|
||||
import { getAvailableOperationsByMetadata } from '../operations';
|
||||
import { IndexPatternPrivateState } from '../types';
|
||||
|
||||
export interface OperationSupportMatrix {
|
||||
operationByField: Partial<Record<string, OperationType[]>>;
|
||||
operationWithoutField: OperationType[];
|
||||
fieldByOperation: Partial<Record<OperationType, string[]>>;
|
||||
}
|
||||
|
||||
type Props = Pick<
|
||||
DatasourceDimensionDropProps<IndexPatternPrivateState>,
|
||||
'layerId' | 'columnId' | 'state' | 'filterOperations'
|
||||
>;
|
||||
|
||||
// TODO: This code has historically been memoized, as a potentially performance
|
||||
// sensitive task. If we can add memoization without breaking the behavior, we should.
|
||||
export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => {
|
||||
const layerId = props.layerId;
|
||||
const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId];
|
||||
|
||||
const filteredOperationsByMetadata = getAvailableOperationsByMetadata(
|
||||
currentIndexPattern
|
||||
).filter((operation) => props.filterOperations(operation.operationMetaData));
|
||||
|
||||
const supportedOperationsByField: Partial<Record<string, OperationType[]>> = {};
|
||||
const supportedOperationsWithoutField: OperationType[] = [];
|
||||
const supportedFieldsByOperation: Partial<Record<OperationType, string[]>> = {};
|
||||
|
||||
filteredOperationsByMetadata.forEach(({ operations }) => {
|
||||
operations.forEach((operation) => {
|
||||
if (operation.type === 'field') {
|
||||
supportedOperationsByField[operation.field] = [
|
||||
...(supportedOperationsByField[operation.field] ?? []),
|
||||
operation.operationType,
|
||||
];
|
||||
|
||||
supportedFieldsByOperation[operation.operationType] = [
|
||||
...(supportedFieldsByOperation[operation.operationType] ?? []),
|
||||
operation.field,
|
||||
];
|
||||
} else if (operation.type === 'none') {
|
||||
supportedOperationsWithoutField.push(operation.operationType);
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
operationByField: _.mapValues(supportedOperationsByField, _.uniq),
|
||||
operationWithoutField: _.uniq(supportedOperationsWithoutField),
|
||||
fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq),
|
||||
};
|
||||
};
|
|
@ -452,22 +452,32 @@ describe('state_helpers', () => {
|
|||
|
||||
describe('getColumnOrder', () => {
|
||||
it('should work for empty columns', () => {
|
||||
expect(getColumnOrder({})).toEqual([]);
|
||||
expect(
|
||||
getColumnOrder({
|
||||
indexPatternId: '',
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('should work for one column', () => {
|
||||
expect(
|
||||
getColumnOrder({
|
||||
col1: {
|
||||
label: 'Value of timestamp',
|
||||
dataType: 'string',
|
||||
isBucketed: false,
|
||||
columnOrder: [],
|
||||
indexPatternId: '',
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Value of timestamp',
|
||||
dataType: 'string',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'date_histogram',
|
||||
sourceField: 'timestamp',
|
||||
params: {
|
||||
interval: 'h',
|
||||
// Private
|
||||
operationType: 'date_histogram',
|
||||
sourceField: 'timestamp',
|
||||
params: {
|
||||
interval: 'h',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -477,41 +487,45 @@ describe('state_helpers', () => {
|
|||
it('should put any number of aggregations before metrics', () => {
|
||||
expect(
|
||||
getColumnOrder({
|
||||
col1: {
|
||||
label: 'Top values of category',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
columnOrder: [],
|
||||
indexPatternId: '',
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Top values of category',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
|
||||
// Private
|
||||
operationType: 'terms',
|
||||
sourceField: 'category',
|
||||
params: {
|
||||
size: 5,
|
||||
orderBy: {
|
||||
type: 'alphabetical',
|
||||
// Private
|
||||
operationType: 'terms',
|
||||
sourceField: 'category',
|
||||
params: {
|
||||
size: 5,
|
||||
orderBy: {
|
||||
type: 'alphabetical',
|
||||
},
|
||||
orderDirection: 'asc',
|
||||
},
|
||||
orderDirection: 'asc',
|
||||
},
|
||||
},
|
||||
col2: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
col2: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
col3: {
|
||||
label: 'Date histogram of timestamp',
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
col3: {
|
||||
label: 'Date histogram of timestamp',
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
|
||||
// Private
|
||||
operationType: 'date_histogram',
|
||||
sourceField: 'timestamp',
|
||||
params: {
|
||||
interval: '1d',
|
||||
// Private
|
||||
operationType: 'date_histogram',
|
||||
sourceField: 'timestamp',
|
||||
params: {
|
||||
interval: '1d',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -521,44 +535,48 @@ describe('state_helpers', () => {
|
|||
it('should reorder aggregations based on suggested priority', () => {
|
||||
expect(
|
||||
getColumnOrder({
|
||||
col1: {
|
||||
label: 'Top values of category',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
indexPatternId: '',
|
||||
columnOrder: [],
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Top values of category',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
|
||||
// Private
|
||||
operationType: 'terms',
|
||||
sourceField: 'category',
|
||||
params: {
|
||||
size: 5,
|
||||
orderBy: {
|
||||
type: 'alphabetical',
|
||||
// Private
|
||||
operationType: 'terms',
|
||||
sourceField: 'category',
|
||||
params: {
|
||||
size: 5,
|
||||
orderBy: {
|
||||
type: 'alphabetical',
|
||||
},
|
||||
orderDirection: 'asc',
|
||||
},
|
||||
orderDirection: 'asc',
|
||||
suggestedPriority: 2,
|
||||
},
|
||||
suggestedPriority: 2,
|
||||
},
|
||||
col2: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
col2: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'bytes',
|
||||
suggestedPriority: 0,
|
||||
},
|
||||
col3: {
|
||||
label: 'Date histogram of timestamp',
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
// Private
|
||||
operationType: 'avg',
|
||||
sourceField: 'bytes',
|
||||
suggestedPriority: 0,
|
||||
},
|
||||
col3: {
|
||||
label: 'Date histogram of timestamp',
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
|
||||
// Private
|
||||
operationType: 'date_histogram',
|
||||
sourceField: 'timestamp',
|
||||
suggestedPriority: 1,
|
||||
params: {
|
||||
interval: '1d',
|
||||
// Private
|
||||
operationType: 'date_histogram',
|
||||
sourceField: 'timestamp',
|
||||
suggestedPriority: 1,
|
||||
params: {
|
||||
interval: '1d',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -25,25 +25,24 @@ export function updateColumnParam<C extends IndexPatternColumn, K extends keyof
|
|||
([_columnId, column]) => column === currentColumn
|
||||
)![0];
|
||||
|
||||
return {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: {
|
||||
...state.layers[layerId],
|
||||
columns: {
|
||||
...state.layers[layerId].columns,
|
||||
[columnId]: {
|
||||
...currentColumn,
|
||||
params: {
|
||||
...currentColumn.params,
|
||||
[paramName]: value,
|
||||
},
|
||||
const layer = state.layers[layerId];
|
||||
|
||||
return mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
columns: {
|
||||
...layer.columns,
|
||||
[columnId]: {
|
||||
...currentColumn,
|
||||
params: {
|
||||
...currentColumn.params,
|
||||
[paramName]: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function adjustColumnReferencesForChangedColumn(
|
||||
|
@ -91,25 +90,29 @@ export function changeColumn<C extends IndexPatternColumn>({
|
|||
updatedColumn.label = oldColumn.label;
|
||||
}
|
||||
|
||||
const layer = {
|
||||
...state.layers[layerId],
|
||||
};
|
||||
|
||||
const newColumns = adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...state.layers[layerId].columns,
|
||||
...layer.columns,
|
||||
[columnId]: updatedColumn,
|
||||
},
|
||||
columnId
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: {
|
||||
...state.layers[layerId],
|
||||
columnOrder: getColumnOrder(newColumns),
|
||||
return mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
columnOrder: getColumnOrder({
|
||||
...layer,
|
||||
columns: newColumns,
|
||||
},
|
||||
}),
|
||||
columns: newColumns,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteColumn({
|
||||
|
@ -125,24 +128,26 @@ export function deleteColumn({
|
|||
delete hypotheticalColumns[columnId];
|
||||
|
||||
const newColumns = adjustColumnReferencesForChangedColumn(hypotheticalColumns, columnId);
|
||||
|
||||
return {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: {
|
||||
...state.layers[layerId],
|
||||
columnOrder: getColumnOrder(newColumns),
|
||||
columns: newColumns,
|
||||
},
|
||||
},
|
||||
const layer = {
|
||||
...state.layers[layerId],
|
||||
columns: newColumns,
|
||||
};
|
||||
|
||||
return mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
...layer,
|
||||
columnOrder: getColumnOrder(layer),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getColumnOrder(columns: Record<string, IndexPatternColumn>): string[] {
|
||||
const entries = Object.entries(columns);
|
||||
|
||||
const [aggregations, metrics] = _.partition(entries, ([id, col]) => col.isBucketed);
|
||||
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]) => {
|
||||
|
@ -156,6 +161,24 @@ export function getColumnOrder(columns: Record<string, IndexPatternColumn>): str
|
|||
.concat(metrics.map(([id]) => id));
|
||||
}
|
||||
|
||||
export function mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer,
|
||||
}: {
|
||||
state: IndexPatternPrivateState;
|
||||
layerId: string;
|
||||
newLayer: Partial<IndexPatternLayer>;
|
||||
}) {
|
||||
return {
|
||||
...state,
|
||||
layers: {
|
||||
...state.layers,
|
||||
[layerId]: { ...state.layers[layerId], ...newLayer },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLayerIndexPattern(
|
||||
layer: IndexPatternLayer,
|
||||
newIndexPattern: IndexPattern
|
||||
|
|
Loading…
Reference in a new issue