diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx index a118183c49061..6e3978a414ec7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx @@ -6,6 +6,7 @@ */ import React, { useMemo, useCallback, useContext, ReactElement } from 'react'; +import { isDraggedField } from '../../../../utils'; import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop'; import { Datasource, @@ -14,23 +15,20 @@ import { DropType, DatasourceLayers, IndexPatternMap, + DragDropOperation, + Visualization, } from '../../../../types'; import { getCustomDropTarget, getAdditionalClassesOnDroppable, getAdditionalClassesOnEnter, - getDropProps, } from './drop_targets_utils'; export function DraggableDimensionButton({ - layerId, - label, - accessorIndex, - groupIndex, - layerIndex, - columnId, + order, group, onDrop, + activeVisualization, onDragStart, onDragEnd, children, @@ -39,100 +37,82 @@ export function DraggableDimensionButton({ datasourceLayers, registerNewButtonRef, indexPatterns, + target, }: { - layerId: string; - groupIndex: number; - layerIndex: number; + target: DragDropOperation & { + id: string; + humanData: { + label: string; + groupLabel: string; + position: number; + layerNumber: number; + }; + }; + order: [2, number, number, number]; onDrop: (source: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType) => void; onDragStart: () => void; onDragEnd: () => void; + activeVisualization: Visualization; group: VisualizationDimensionGroupConfig; - label: string; children: ReactElement; layerDatasource?: Datasource; datasourceLayers: DatasourceLayers; state: unknown; - accessorIndex: number; - columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; indexPatterns: IndexPatternMap; }) { const { dragging } = useContext(DragContext); - const sharedDatasource = - !isOperation(dragging) || - datasourceLayers?.[dragging.layerId]?.datasourceId === datasourceLayers?.[layerId]?.datasourceId - ? layerDatasource - : undefined; + let getDropProps; - const dropProps = getDropProps( - { - state, - source: dragging, - target: { - layerId, - columnId, - groupId: group.groupId, - filterOperations: group.filterOperations, - prioritizedOperation: group.prioritizedOperation, - }, - indexPatterns, - }, - sharedDatasource - ); + if (dragging) { + if (!layerDatasource) { + getDropProps = activeVisualization.getDropProps; + } else if ( + isDraggedField(dragging) || + (isOperation(dragging) && + layerDatasource && + datasourceLayers?.[dragging.layerId]?.datasourceId === + datasourceLayers?.[target.layerId]?.datasourceId) + ) { + getDropProps = layerDatasource.getDropProps; + } + } + + const { dropTypes, nextLabel } = getDropProps?.({ + state, + source: dragging, + target, + indexPatterns, + }) || { dropTypes: [], nextLabel: '' }; - const dropTypes = dropProps?.dropTypes; - const nextLabel = dropProps?.nextLabel; const canDuplicate = !!( - dropTypes && - (dropTypes.includes('replace_duplicate_incompatible') || - dropTypes.includes('replace_duplicate_compatible')) + dropTypes.includes('replace_duplicate_incompatible') || + dropTypes.includes('replace_duplicate_compatible') ); const canSwap = !!( - dropTypes && - (dropTypes.includes('swap_incompatible') || dropTypes.includes('swap_compatible')) + dropTypes.includes('swap_incompatible') || dropTypes.includes('swap_compatible') ); const canCombine = Boolean( - dropTypes && - (dropTypes.includes('combine_compatible') || - dropTypes.includes('field_combine') || - dropTypes.includes('combine_incompatible')) + dropTypes.includes('combine_compatible') || + dropTypes.includes('field_combine') || + dropTypes.includes('combine_incompatible') ); const value = useMemo( () => ({ - columnId, - groupId: group.groupId, - layerId, - id: columnId, - filterOperations: group.filterOperations, + ...target, humanData: { + ...target.humanData, canSwap, canDuplicate, canCombine, - label, - groupLabel: group.groupLabel, - position: accessorIndex + 1, nextLabel: nextLabel || '', - layerNumber: layerIndex + 1, }, }), - [ - columnId, - group.groupId, - accessorIndex, - layerId, - label, - group.groupLabel, - nextLabel, - group.filterOperations, - canDuplicate, - canSwap, - canCombine, - layerIndex, - ] + [target, nextLabel, canDuplicate, canSwap, canCombine] ); const reorderableGroup = useMemo( @@ -144,8 +124,8 @@ export function DraggableDimensionButton({ ); const registerNewButtonRefMemoized = useCallback( - (el) => registerNewButtonRef(columnId, el), - [registerNewButtonRef, columnId] + (el) => registerNewButtonRef(target.columnId, el), + [registerNewButtonRef, target.columnId] ); const handleOnDrop = useCallback( @@ -162,7 +142,7 @@ export function DraggableDimensionButton({ getCustomDropTarget={getCustomDropTarget} getAdditionalClassesOnEnter={getAdditionalClassesOnEnter} getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable} - order={[2, layerIndex, groupIndex, accessorIndex]} + order={order} draggable dragType={isOperation(dragging) ? 'move' : 'copy'} dropTypes={dropTypes} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx deleted file mode 100644 index 17907ac19c4bc..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx +++ /dev/null @@ -1,128 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getDropProps } from './drop_targets_utils'; -import { createMockDatasource } from '../../../../mocks'; - -describe('getDropProps', () => { - it('should run datasource getDropProps if exists', () => { - const mockDatasource = createMockDatasource('testDatasource'); - getDropProps( - { - state: 'datasourceState', - target: { - columnId: 'col1', - groupId: 'x', - layerId: 'first', - filterOperations: () => true, - }, - source: { - columnId: 'col1', - groupId: 'x', - layerId: 'first', - id: 'annotationColumn2', - humanData: { label: 'Event' }, - }, - indexPatterns: {}, - }, - mockDatasource - ); - expect(mockDatasource.getDropProps).toHaveBeenCalled(); - }); - describe('no datasource', () => { - it('returns reorder for the same group existing columns', () => { - expect( - getDropProps({ - state: 'datasourceState', - target: { - columnId: 'annotationColumn', - groupId: 'xAnnotations', - layerId: 'second', - filterOperations: () => true, - }, - source: { - columnId: 'annotationColumn2', - groupId: 'xAnnotations', - layerId: 'second', - id: 'annotationColumn2', - humanData: { label: 'Event' }, - }, - indexPatterns: {}, - }) - ).toEqual({ dropTypes: ['reorder'] }); - }); - it('returns duplicate for the same group existing column and not existing column', () => { - expect( - getDropProps({ - state: 'datasourceState', - target: { - columnId: 'annotationColumn', - groupId: 'xAnnotations', - layerId: 'second', - isNewColumn: true, - filterOperations: () => true, - }, - source: { - columnId: 'annotationColumn2', - groupId: 'xAnnotations', - layerId: 'second', - id: 'annotationColumn2', - humanData: { label: 'Event' }, - }, - indexPatterns: {}, - }) - ).toEqual({ dropTypes: ['duplicate_compatible'] }); - }); - it('returns replace_duplicate and replace for replacing to different layer', () => { - expect( - getDropProps({ - state: 'datasourceState', - target: { - columnId: 'annotationColumn', - groupId: 'xAnnotations', - layerId: 'first', - filterOperations: () => true, - }, - source: { - columnId: 'annotationColumn2', - groupId: 'xAnnotations', - layerId: 'second', - id: 'annotationColumn2', - humanData: { label: 'Event' }, - }, - indexPatterns: {}, - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], - }); - }); - it('returns duplicate and move for replacing to different layer for empty column', () => { - expect( - getDropProps({ - state: 'datasourceState', - target: { - columnId: 'annotationColumn', - groupId: 'xAnnotations', - layerId: 'first', - isNewColumn: true, - filterOperations: () => true, - }, - source: { - columnId: 'annotationColumn2', - groupId: 'xAnnotations', - layerId: 'second', - id: 'annotationColumn2', - humanData: { label: 'Event' }, - }, - indexPatterns: {}, - }) - ).toEqual({ - dropTypes: ['move_compatible', 'duplicate_compatible'], - }); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx index 5f3fd2d4a73b5..3094a07cf3290 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx @@ -9,12 +9,10 @@ import React from 'react'; import classNames from 'classnames'; import { EuiIcon, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DragDropIdentifier, DraggingIdentifier } from '../../../../drag_drop'; +import { DragDropIdentifier } from '../../../../drag_drop'; import { - Datasource, DropType, FramePublicAPI, - GetDropPropsArgs, isOperation, Visualization, DragDropOperation, @@ -140,53 +138,6 @@ export const getAdditionalClassesOnDroppable = (dropType?: string) => { } }; -const isOperationFromCompatibleGroup = (op1?: DraggingIdentifier, op2?: DragDropOperation) => { - return ( - isOperation(op1) && - isOperation(op2) && - op1.columnId !== op2.columnId && - op1.groupId === op2.groupId && - op1.layerId !== op2.layerId - ); -}; - -export const isOperationFromTheSameGroup = (op1?: DraggingIdentifier, op2?: DragDropOperation) => { - return ( - isOperation(op1) && - isOperation(op2) && - op1.columnId !== op2.columnId && - op1.groupId === op2.groupId && - op1.layerId === op2.layerId - ); -}; - -export function getDropPropsForSameGroup( - isNewColumn?: boolean -): { dropTypes: DropType[]; nextLabel?: string } | undefined { - return !isNewColumn ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; -} - -export const getDropProps = ( - dropProps: GetDropPropsArgs, - sharedDatasource?: Datasource -): { dropTypes: DropType[]; nextLabel?: string } | undefined => { - if (sharedDatasource) { - return sharedDatasource?.getDropProps(dropProps); - } else { - if (isOperationFromTheSameGroup(dropProps.source, dropProps.target)) { - return getDropPropsForSameGroup(dropProps.target.isNewColumn); - } - if (isOperationFromCompatibleGroup(dropProps.source, dropProps.target)) { - return { - dropTypes: dropProps.target.isNewColumn - ? ['move_compatible', 'duplicate_compatible'] - : ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], - }; - } - } - return; -}; - export interface OnVisDropProps { prevState: T; target: DragDropOperation; @@ -215,7 +166,6 @@ export function onDropForVisualization( frame, }); - // remove source if ( isOperation(source) && (dropType === 'move_compatible' || diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index b6d7e58b0f7e7..8b1fe4082b31c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -9,6 +9,7 @@ import React, { useMemo, useState, useEffect, useContext } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { isDraggedField } from '../../../../utils'; import { generateId } from '../../../../id_generator'; import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop'; @@ -19,16 +20,10 @@ import { DatasourceLayers, isOperation, IndexPatternMap, + DragDropOperation, + Visualization, } from '../../../../types'; -import { - getCustomDropTarget, - getAdditionalClassesOnDroppable, - getDropProps, -} from './drop_targets_utils'; - -const label = i18n.translate('xpack.lens.indexPattern.emptyDimensionButton', { - defaultMessage: 'Empty dimension', -}); +import { getCustomDropTarget, getAdditionalClassesOnDroppable } from './drop_targets_utils'; interface EmptyButtonProps { columnId: string; @@ -106,91 +101,81 @@ export function EmptyDimensionButton({ group, layerDatasource, state, - layerId, - groupIndex, - layerIndex, onClick, onDrop, datasourceLayers, indexPatterns, + activeVisualization, + order, + target, }: { - layerId: string; - groupIndex: number; - layerIndex: number; - onDrop: (source: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType) => void; - onClick: (id: string) => void; + order: [2, number, number, number]; group: VisualizationDimensionGroupConfig; layerDatasource?: Datasource; datasourceLayers: DatasourceLayers; state: unknown; + onDrop: (source: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType) => void; + onClick: (id: string) => void; indexPatterns: IndexPatternMap; + activeVisualization: Visualization; + target: Omit & { + humanData: { + groupLabel: string; + position: number; + layerNumber: number; + label: string; + }; + }; }) { const { dragging } = useContext(DragContext); - const sharedDatasource = - !isOperation(dragging) || - datasourceLayers?.[dragging.layerId]?.datasourceId === datasourceLayers?.[layerId]?.datasourceId - ? layerDatasource - : undefined; - const itemIndex = group.accessors.length; + let getDropProps; + + if (dragging) { + if (!layerDatasource) { + getDropProps = activeVisualization.getDropProps; + } else if ( + isDraggedField(dragging) || + (isOperation(dragging) && + layerDatasource && + datasourceLayers?.[dragging.layerId]?.datasourceId === + datasourceLayers?.[target.layerId]?.datasourceId) + ) { + getDropProps = layerDatasource.getDropProps; + } + } const [newColumnId, setNewColumnId] = useState(generateId()); useEffect(() => { setNewColumnId(generateId()); - }, [itemIndex]); - - const dropProps = getDropProps( - { - state, - source: dragging, - target: { - layerId, - columnId: newColumnId, - groupId: group.groupId, - filterOperations: group.filterOperations, - prioritizedOperation: group.prioritizedOperation, - isNewColumn: true, - }, - indexPatterns, - }, - sharedDatasource - ); + }, [group.accessors.length]); - const dropTypes = dropProps?.dropTypes; - const nextLabel = dropProps?.nextLabel; + const { dropTypes, nextLabel } = getDropProps?.({ + state, + source: dragging, + target: { + ...target, + columnId: newColumnId, + }, + indexPatterns, + }) || { dropTypes: [], nextLabel: '' }; const canDuplicate = !!( - dropTypes && - (dropTypes.includes('duplicate_compatible') || dropTypes.includes('duplicate_incompatible')) + dropTypes.includes('duplicate_compatible') || dropTypes.includes('duplicate_incompatible') ); const value = useMemo( () => ({ + ...target, columnId: newColumnId, - groupId: group.groupId, - layerId, - filterOperations: group.filterOperations, id: newColumnId, humanData: { - label, - groupLabel: group.groupLabel, - position: itemIndex + 1, + ...target.humanData, nextLabel: nextLabel || '', canDuplicate, - layerNumber: layerIndex + 1, }, }), - [ - newColumnId, - group.groupId, - layerId, - group.groupLabel, - group.filterOperations, - itemIndex, - nextLabel, - canDuplicate, - layerIndex, - ] + [newColumnId, target, nextLabel, canDuplicate] ); const handleOnDrop = React.useCallback( @@ -209,7 +194,7 @@ export function EmptyDimensionButton({ layerDatasource.getUsedDataView(layerDatasourceState, layer)), - defaultDataView: layerDatasource.getCurrentIndexPatternId(layerDatasourceState), + defaultDataView: layerDatasource.getUsedDataView(layerDatasourceState), } as ActionExecutionContext); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index cc748df7c3ecd..e40281fa1f3ea 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -458,18 +458,34 @@ export function LayerPanel( const { columnId } = accessorConfig; return ( setHideTooltip(true)} onDragEnd={() => setHideTooltip(false)} onDrop={onDrop} @@ -567,10 +583,27 @@ export function LayerPanel( {group.supportsMoreColumns ? ( !op.isBucketed, id: 'col1', humanData: { label: 'Column 1' }, + indexPatternId: 'first', }, numericalOnly: { layerId: 'first', @@ -257,6 +259,7 @@ export const mockedDndOperations = { filterOperations: (op: OperationMetadata) => op.dataType === 'number', id: 'col1', humanData: { label: 'Column 1' }, + indexPatternId: 'first', }, bucket: { columnId: 'col2', @@ -265,6 +268,7 @@ export const mockedDndOperations = { id: 'col2', humanData: { label: 'Column 2' }, filterOperations: (op: OperationMetadata) => op.isBucketed, + indexPatternId: 'first', }, staticValue: { columnId: 'col1', @@ -273,6 +277,7 @@ export const mockedDndOperations = { id: 'col1', humanData: { label: 'Column 2' }, filterOperations: (op: OperationMetadata) => !!op.isStaticValue, + indexPatternId: 'first', }, bucket2: { columnId: 'col3', @@ -282,6 +287,7 @@ export const mockedDndOperations = { humanData: { label: '', }, + indexPatternId: 'first', }, metricC: { columnId: 'col4', @@ -292,5 +298,6 @@ export const mockedDndOperations = { label: '', }, filterOperations: (op: OperationMetadata) => !op.isBucketed, + indexPatternId: 'first', }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts index 6156be3570031..3b468181db6df 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts @@ -1562,12 +1562,14 @@ describe('IndexPatternDimensionEditorPanel: onDrop', () => { groupId: 'x', layerId: 'first', filterOperations: (op: OperationMetadata) => op.isBucketed, + indexPatternId: 'indexPattern1', }, target: { filterOperations: (op: OperationMetadata) => op.isBucketed, columnId: 'newCol', groupId: 'x', layerId: 'second', + indexPatternId: 'indexPattern1', }, dimensionGroups: defaultDimensionGroups, dropType: 'move_compatible', @@ -2161,6 +2163,7 @@ describe('IndexPatternDimensionEditorPanel: onDrop', () => { groupId: 'y', layerId: 'second', filterOperations: (op) => !op.isBucketed, + indexPatternId: 'test', }, }; @@ -2224,6 +2227,7 @@ describe('IndexPatternDimensionEditorPanel: onDrop', () => { groupId: 'y', layerId: 'second', filterOperations: (op) => !op.isBucketed, + indexPatternId: 'test', }, }) ).toEqual(true); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts index 8ea027a3da98f..77444e8ae59ee 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { isDraggedField } from '../../../utils'; import { DatasourceDimensionDropHandlerProps, DragDropOperation, @@ -12,6 +13,7 @@ import { isOperation, StateSetter, VisualizationDimensionGroupConfig, + DraggedField, } from '../../../types'; import { insertOrReplaceColumn, @@ -25,9 +27,8 @@ import { deleteColumnInLayers, } from '../../operations'; import { mergeLayer, mergeLayers } from '../../state_helpers'; -import { isDraggedField } from '../../pure_utils'; import { getNewOperation, getField } from './get_drop_props'; -import { IndexPatternPrivateState, DraggedField, DataViewDragDropOperation } from '../../types'; +import { IndexPatternPrivateState, DataViewDragDropOperation } from '../../types'; interface DropHandlerProps { state: IndexPatternPrivateState; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 29f666a4ff6eb..181459e0171d4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -32,10 +32,9 @@ import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { AddFieldFilterHandler, FieldStats } from '@kbn/unified-field-list-plugin/public'; import { generateFilters } from '@kbn/data-plugin/public'; import { DragDrop, DragDropIdentifier } from '../drag_drop'; -import { DatasourceDataPanelProps, DataType } from '../types'; +import { DatasourceDataPanelProps, DataType, DraggedField } from '../types'; import { DOCUMENT_FIELD_NAME } from '../../common'; import type { IndexPattern, IndexPatternField } from '../types'; -import type { DraggedField } from './types'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; import type { LensAppServices } from '../app_plugin/types'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 20d0df1358be7..744c21a56c6b4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -68,7 +68,8 @@ import { isColumnInvalid, cloneLayer, } from './utils'; -import { normalizeOperationDataType, isDraggedField } from './pure_utils'; +import { isDraggedField } from '../utils'; +import { normalizeOperationDataType } from './pure_utils'; import { LayerPanel } from './layerpanel'; import { DateHistogramIndexPatternColumn, @@ -176,10 +177,6 @@ export function getIndexPatternDatasource({ return extractReferences(state); }, - getCurrentIndexPatternId(state: IndexPatternPrivateState) { - return state.currentIndexPatternId; - }, - insertLayer(state: IndexPatternPrivateState, newLayerId: string) { return { ...state, @@ -774,7 +771,10 @@ export function getIndexPatternDatasource({ injectReferences(persistableState1, references1), injectReferences(persistableState2, references2) ), - getUsedDataView: (state: IndexPatternPrivateState, layerId: string) => { + getUsedDataView: (state: IndexPatternPrivateState, layerId?: string) => { + if (!layerId) { + return state.currentIndexPatternId; + } return state.layers[layerId].indexPatternId; }, getUsedDataViews: (state) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 2df8300eb6481..1a77cd253424f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -190,6 +190,7 @@ describe('state_helpers', () => { layerId: 'layer', dataView: indexPattern, filterOperations: () => true, + indexPatternId: '1', }, target: { columnId: 'copy', @@ -197,6 +198,7 @@ describe('state_helpers', () => { dataView: indexPattern, layerId: 'layer', filterOperations: () => true, + indexPatternId: '1', }, shouldDeleteSource: false, }).layer diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/pure_utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_utils.ts index e1fd78e0b1713..39b4bcdf49229 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/pure_utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/pure_utils.ts @@ -6,7 +6,7 @@ */ import type { DataType, IndexPattern, IndexPatternField } from '../types'; -import type { DraggedField, IndexPatternLayer } from './types'; +import type { IndexPatternLayer } from './types'; import type { BaseIndexPatternColumn, FieldBasedIndexPatternColumn, @@ -53,11 +53,3 @@ export function sortByField(columns: C[]) { return column1.operationType.localeCompare(column2.operationType); }); } - -export function isDraggedField(fieldCandidate: unknown): fieldCandidate is DraggedField { - return ( - typeof fieldCandidate === 'object' && - fieldCandidate !== null && - ['id', 'field', 'indexPatternId'].every((prop) => prop in fieldCandidate) - ); -} diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts index 3d169b643c2ce..65d001a726b8c 100644 --- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -41,7 +41,6 @@ export function createMockDatasource(id: string): DatasourceMock { initialize: jest.fn((_state?) => {}), renderDataPanel: jest.fn(), renderLayerPanel: jest.fn(), - getCurrentIndexPatternId: jest.fn(), toExpression: jest.fn((_frame, _state, _indexPatterns) => null), insertLayer: jest.fn((_state, _newLayerId) => ({})), removeLayer: jest.fn((_state, _layerId) => {}), diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts index fc536b30ddac6..f83786238f628 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts @@ -279,7 +279,7 @@ describe('lensSlice', () => { removeLayer: (layerIds: unknown, layerId: string) => (layerIds as string[]).filter((id: string) => id !== layerId), insertLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId], - getCurrentIndexPatternId: jest.fn(() => 'indexPattern1'), + getUsedDataView: jest.fn(() => 'indexPattern1'), }; }; const datasourceStates = { diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 725c60bfc22cb..38aee718a536f 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -377,7 +377,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { ); state.stagedPreview = undefined; // reuse the activeDatasource current dataView id for the moment - const currentDataViewsId = activeDataSource.getCurrentIndexPatternId( + const currentDataViewsId = activeDataSource.getUsedDataView( state.datasourceStates[state.activeDatasourceId!].state ); state.visualization.state = @@ -928,7 +928,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { const activeVisualization = visualizationMap[state.visualization.activeId]; const activeDatasource = datasourceMap[state.activeDatasourceId]; // reuse the active datasource dataView id for the new layer - const currentDataViewsId = activeDatasource.getCurrentIndexPatternId( + const currentDataViewsId = activeDatasource.getUsedDataView( state.datasourceStates[state.activeDatasourceId!].state ); const visualizationState = activeVisualization.appendLayer!( diff --git a/x-pack/plugins/lens/public/text_based_languages_datasource/text_based_languages.tsx b/x-pack/plugins/lens/public/text_based_languages_datasource/text_based_languages.tsx index 5a03500c76fbf..4857d3eed3e32 100644 --- a/x-pack/plugins/lens/public/text_based_languages_datasource/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/text_based_languages_datasource/text_based_languages.tsx @@ -224,10 +224,6 @@ export function getTextBasedLanguagesDatasource({ getLayers(state: TextBasedLanguagesPrivateState) { return state && state.layers ? Object.keys(state?.layers) : []; }, - getCurrentIndexPatternId(state: TextBasedLanguagesPrivateState) { - const layers = Object.values(state.layers); - return layers?.[0]?.index; - }, isTimeBased: (state, indexPatterns) => { if (!state) return false; const { layers } = state; @@ -238,7 +234,11 @@ export function getTextBasedLanguagesDatasource({ }) ); }, - getUsedDataView: (state: TextBasedLanguagesPrivateState, layerId: string) => { + getUsedDataView: (state: TextBasedLanguagesPrivateState, layerId?: string) => { + if (!layerId) { + const layers = Object.values(state.layers); + return layers?.[0]?.index; + } return state.layers[layerId].index; }, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 32fc21cc4a61d..486ddef88ee99 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -228,14 +228,7 @@ export type VisualizeEditorContext = { export interface GetDropPropsArgs { state: T; source?: DraggingIdentifier; - target: { - layerId: string; - groupId: string; - columnId: string; - filterOperations: (meta: OperationMetadata) => boolean; - prioritizedOperation?: string; - isNewColumn?: boolean; - }; + target: DragDropOperation; indexPatterns: IndexPatternMap; } @@ -258,7 +251,6 @@ export interface Datasource { // Given the current state, which parts should be saved? getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; - getCurrentIndexPatternId: (state: T) => string; getUnifiedSearchErrors?: (state: T) => Error[]; insertLayer: (state: T, newLayerId: string) => T; @@ -441,7 +433,7 @@ export interface Datasource { /** * Get the used DataView value from state */ - getUsedDataView: (state: T, layerId: string) => string; + getUsedDataView: (state: T, layerId?: string) => string; /** * Get all the used DataViews from state */ @@ -582,8 +574,16 @@ export interface DragDropOperation { groupId: string; columnId: string; filterOperations: (operation: OperationMetadata) => boolean; + indexPatternId?: string; + isNewColumn?: boolean; + prioritizedOperation?: string; } +export type DraggedField = DragDropIdentifier & { + field: IndexPatternField; + indexPatternId: string; +}; + export function isOperation(operationCandidate: unknown): operationCandidate is DragDropOperation { return ( typeof operationCandidate === 'object' && @@ -892,6 +892,7 @@ export interface Visualization { */ initialize: (addNewLayer: () => string, state?: T, mainPalette?: PaletteOutput) => T; + getUsedDataView?: (state: T, layerId: string) => string | undefined; /** * Retrieve the used DataViews in the visualization */ @@ -1022,6 +1023,10 @@ export interface Visualization { group?: VisualizationDimensionGroupConfig; }) => T; + getDropProps?: ( + dropProps: GetDropPropsArgs + ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; + /** * Additional editor that gets rendered inside the dimension popover. * This can be used to configure dimension-specific options diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 8f25379c0e21e..181ea104ffa71 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -15,15 +15,19 @@ import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public' import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { BrushTriggerEvent, ClickTriggerEvent } from '@kbn/charts-plugin/public'; import type { Document } from './persistence/saved_object_store'; -import type { +import { Datasource, DatasourceMap, Visualization, IndexPatternMap, IndexPatternRef, + DraggedField, + DragDropOperation, + isOperation, } from './types'; import type { DatasourceStates, VisualizationState } from './state_management'; import { IndexPatternServiceAPI } from './data_views_service/service'; +import { DraggingIdentifier } from './drag_drop'; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { @@ -126,7 +130,7 @@ export function getIndexPatternsIds({ const references: SavedObjectReference[] = []; Object.entries(activeDatasources).forEach(([id, datasource]) => { const { savedObjectReferences } = datasource.getPersistableState(datasourceStates[id].state); - const indexPatternId = datasource.getCurrentIndexPatternId(datasourceStates[id].state); + const indexPatternId = datasource.getUsedDataView(datasourceStates[id].state); currentIndexPatternId = indexPatternId; references.push(...savedObjectReferences); }); @@ -242,3 +246,34 @@ export function renewIDs( */ export const DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS = 'lensDontCloseDimensionContainerOnClick'; + +export function isDraggedField(fieldCandidate: unknown): fieldCandidate is DraggedField { + return ( + typeof fieldCandidate === 'object' && + fieldCandidate !== null && + ['id', 'field', 'indexPatternId'].every((prop) => prop in fieldCandidate) + ); +} + +export const isOperationFromCompatibleGroup = ( + op1?: DraggingIdentifier, + op2?: DragDropOperation +) => { + return ( + isOperation(op1) && + isOperation(op2) && + op1.columnId !== op2.columnId && + op1.groupId === op2.groupId && + op1.layerId !== op2.layerId + ); +}; + +export const isOperationFromTheSameGroup = (op1?: DraggingIdentifier, op2?: DragDropOperation) => { + return ( + isOperation(op1) && + isOperation(op2) && + op1.columnId !== op2.columnId && + op1.groupId === op2.groupId && + op1.layerId === op2.layerId + ); +}; diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx index baaed78ec0237..3c54d67c49131 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx @@ -10,10 +10,12 @@ import moment from 'moment'; import { defaultAnnotationColor, defaultAnnotationRangeColor, + isQueryAnnotationConfig, isRangeAnnotationConfig, } from '@kbn/event-annotation-plugin/public'; import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { IconChartBarAnnotations } from '@kbn/chart-icons'; +import { isDraggedField } from '../../../utils'; import { layerTypes } from '../../../../common'; import type { FramePublicAPI, Visualization } from '../../../types'; import { isHorizontalChart } from '../state_helpers'; @@ -125,7 +127,7 @@ export const getAnnotationsSupportedLayer = ( }; }; -const getDefaultAnnotationConfig = (id: string, timestamp: string): EventAnnotationConfig => ({ +const getDefaultManualAnnotation = (id: string, timestamp: string): EventAnnotationConfig => ({ label: defaultAnnotationLabel, type: 'manual', key: { @@ -136,13 +138,32 @@ const getDefaultAnnotationConfig = (id: string, timestamp: string): EventAnnotat id, }); +const getDefaultQueryAnnotation = ( + id: string, + fieldName: string, + timeField: string +): EventAnnotationConfig => ({ + filter: { + type: 'kibana_query', + query: `${fieldName}: *`, + language: 'kuery', + }, + timeField, + type: 'query', + key: { + type: 'point_in_time', + }, + id, + label: `${fieldName}: *`, +}); + const createCopiedAnnotation = ( newId: string, timestamp: string, source?: EventAnnotationConfig ): EventAnnotationConfig => { if (!source) { - return getDefaultAnnotationConfig(newId, timestamp); + return getDefaultManualAnnotation(newId, timestamp); } return { ...source, @@ -158,17 +179,78 @@ export const onAnnotationDrop: Visualization['onDrop'] = ({ dropType, }) => { const targetLayer = prevState.layers.find((l) => l.layerId === target.layerId); - const sourceLayer = prevState.layers.find((l) => l.layerId === source.layerId); - if ( - !targetLayer || - !isAnnotationsLayer(targetLayer) || - !sourceLayer || - !isAnnotationsLayer(sourceLayer) - ) { + if (!targetLayer || !isAnnotationsLayer(targetLayer)) { return prevState; } const targetAnnotation = targetLayer.annotations.find(({ id }) => id === target.columnId); + const targetDataView = frame.dataViews.indexPatterns[targetLayer.indexPatternId]; + + if (isDraggedField(source)) { + const timeField = targetDataView.timeFieldName; + switch (dropType) { + case 'field_add': + if (targetAnnotation || !timeField) { + return prevState; + } + return { + ...prevState, + layers: prevState.layers.map( + (l): XYLayerConfig => + l.layerId === target.layerId + ? { + ...targetLayer, + annotations: [ + ...targetLayer.annotations, + getDefaultQueryAnnotation(target.columnId, source.field.name, timeField), + ], + } + : l + ), + }; + case 'field_replace': + if (!targetAnnotation || !timeField) { + return prevState; + } + + return { + ...prevState, + layers: prevState.layers.map( + (l): XYLayerConfig => + l.layerId === target.layerId + ? { + ...targetLayer, + annotations: [ + ...targetLayer.annotations.map((a) => + a === targetAnnotation + ? { + ...targetAnnotation, + ...getDefaultQueryAnnotation( + target.columnId, + source.field.name, + timeField + ), + } + : a + ), + ], + } + : l + ), + }; + } + + return prevState; + } + + const sourceLayer = prevState.layers.find((l) => l.layerId === source.layerId); + if (!sourceLayer || !isAnnotationsLayer(sourceLayer)) { + return prevState; + } const sourceAnnotation = sourceLayer.annotations.find(({ id }) => id === source.columnId); + const sourceDataView = frame.dataViews.indexPatterns[sourceLayer.indexPatternId]; + if (sourceDataView !== targetDataView && isQueryAnnotationConfig(sourceAnnotation)) { + return prevState; + } switch (dropType) { case 'reorder': if (!targetAnnotation || !sourceAnnotation || source.layerId !== target.layerId) { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 556a89c9a8553..c62d2c1195e5f 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -338,7 +338,11 @@ describe('xy_visualization', () => { let frame: ReturnType; beforeEach(() => { - frame = createMockFramePublicAPI(); + frame = createMockFramePublicAPI({ + dataViews: createMockDataViewsState({ + indexPatterns: { indexPattern1: createMockedIndexPattern() }, + }), + }); mockDatasource = createMockDatasource('testDatasource'); mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ @@ -494,309 +498,650 @@ describe('xy_visualization', () => { ], }); }); - it('should copy previous column if passed and assign a new id', () => { - expect( - xyVisualization.onDrop!({ - frame, - prevState: { - ...exampleState(), - layers: [ - { - layerId: 'annotation', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation2], - ignoreGlobalFilters: true, + + describe('getDropProps', () => { + it('dragging operation: returns reorder for the same group existing columns', () => { + expect( + xyVisualization.getDropProps?.({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'second', + filterOperations: () => true, + indexPatternId: '1', + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + indexPatternId: '1', + }, + indexPatterns: {}, + }) + ).toEqual({ dropTypes: ['reorder'] }); + }); + it('dragging operation: returns duplicate for the same group existing column and not existing column', () => { + expect( + xyVisualization.getDropProps?.({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'second', + isNewColumn: true, + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + indexPatternId: 'indexPattern1', + }, + indexPatterns: {}, + }) + ).toEqual({ dropTypes: ['duplicate_compatible'] }); + }); + it('dragging operation: returns replace_duplicate and replace for replacing to different layer', () => { + expect( + xyVisualization.getDropProps?.({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'first', + filterOperations: () => true, + indexPatternId: '1', + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + indexPatternId: '1', + }, + indexPatterns: {}, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], + }); + }); + it('dragging operation: returns duplicate and move for replacing to different layer for empty column', () => { + expect( + xyVisualization.getDropProps?.({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'first', + isNewColumn: true, + indexPatternId: 'indexPattern1', + filterOperations: () => true, + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + indexPatternId: 'indexPattern1', + }, + indexPatterns: {}, + }) + ).toEqual({ + dropTypes: ['move_compatible', 'duplicate_compatible'], + }); + }); + it('dragging operation: does not allow to drop for different operations on different data views', () => { + expect( + xyVisualization.getDropProps?.({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'first', + isNewColumn: true, + indexPatternId: 'indexPattern1', + filterOperations: () => true, + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + indexPatternId: 'indexPattern2', + }, + indexPatterns: {}, + }) + ).toEqual(undefined); + }); + it('dragging field: should add a new dimension when dragged to a new dimension', () => { + expect( + xyVisualization.getDropProps?.({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'first', + isNewColumn: true, + indexPatternId: 'indexPattern1', + filterOperations: () => true, + }, + source: { + field: { + name: 'agent.keyword', + displayName: 'agent.keyword', }, - ], - }, - dropType: 'duplicate_compatible', - source: { - layerId: 'annotation', - groupId: 'xAnnotation', - columnId: 'an2', - id: 'an2', - humanData: { label: 'an2' }, - }, - target: { - layerId: 'annotation', - groupId: 'xAnnotation', - columnId: 'newColId', - filterOperations: Boolean, - }, - }).layers[0] - ).toEqual({ - layerId: 'annotation', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation2, { ...exampleAnnotation2, id: 'newColId' }], - ignoreGlobalFilters: true, + indexPatternId: 'indexPattern1', + id: 'agent.keyword', + humanData: { + label: 'agent.keyword', + position: 2, + }, + }, + indexPatterns: {}, + }) + ).toEqual({ dropTypes: ['field_add'] }); }); - }); - it('should reorder a dimension to a annotation layer', () => { - expect( - xyVisualization.onDrop!({ - frame, - prevState: { - ...exampleState(), - layers: [ - { - layerId: 'annotation', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation, exampleAnnotation2], - ignoreGlobalFilters: true, + it('dragging field: should replace an existing dimension when dragged to a dimension', () => { + expect( + xyVisualization.getDropProps?.({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'first', + indexPatternId: 'indexPattern1', + filterOperations: () => true, + }, + source: { + field: { + name: 'agent.keyword', + displayName: 'agent.keyword', }, - ], - }, - source: { - layerId: 'annotation', - groupId: 'xAnnotation', - columnId: 'an2', - id: 'an2', - humanData: { label: 'label' }, - filterOperations: () => true, - }, - target: { - layerId: 'annotation', - groupId: 'xAnnotation', - columnId: 'an1', - filterOperations: () => true, - }, - dropType: 'reorder', - }).layers[0] - ).toEqual({ - layerId: 'annotation', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation2, exampleAnnotation], - ignoreGlobalFilters: true, + indexPatternId: 'indexPattern1', + id: 'agent.keyword', + humanData: { + label: 'agent.keyword', + position: 2, + }, + }, + indexPatterns: {}, + }) + ).toEqual({ dropTypes: ['field_replace'] }); + }); + it('dragging field: should not allow to drop when data view conflict', () => { + expect( + xyVisualization.getDropProps?.({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'first', + indexPatternId: 'indexPattern1', + filterOperations: () => true, + }, + source: { + field: { + name: 'agent.keyword', + displayName: 'agent.keyword', + }, + indexPatternId: 'indexPattern2', + id: 'agent.keyword', + humanData: { + label: 'agent.keyword', + position: 2, + }, + }, + indexPatterns: {}, + }) + ).toEqual(undefined); }); }); - it('should duplicate the annotations and replace the target in another annotation layer', () => { - expect( - xyVisualization.onDrop!({ - frame, - prevState: { - ...exampleState(), - layers: [ - { - layerId: 'first', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation], - ignoreGlobalFilters: true, + describe('onDrop', () => { + it('dragging field: should add a new dimension when dragged to a new dimension', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, + }, + ], + }, + dropType: 'field_add', + source: { + field: { + name: 'agent.keyword', }, - { - layerId: 'second', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation2], - ignoreGlobalFilters: true, + indexPatternId: 'indexPattern1', + id: 'agent.keyword', + humanData: { + label: 'agent.keyword', + position: 2, }, - ], - }, - source: { - layerId: 'first', - groupId: 'xAnnotation', - columnId: 'an1', - id: 'an1', - humanData: { label: 'label' }, - filterOperations: () => true, - }, - target: { - layerId: 'second', - groupId: 'xAnnotation', - columnId: 'an2', - filterOperations: () => true, - }, - dropType: 'replace_duplicate_compatible', - }).layers - ).toEqual([ - { - layerId: 'first', + }, + target: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'newColId', + filterOperations: Boolean, + indexPatternId: 'indexPattern1', + }, + }).layers[0] + ).toEqual({ + layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation], + annotations: [ + exampleAnnotation2, + { + filter: { + language: 'kuery', + query: 'agent.keyword: *', + type: 'kibana_query', + }, + id: 'newColId', + key: { + type: 'point_in_time', + }, + label: 'agent.keyword: *', + timeField: 'timestamp', + type: 'query', + }, + ], ignoreGlobalFilters: true, - }, - { - layerId: 'second', + }); + }); + it('dragging field: should replace an existing dimension when dragged to a dimension', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, + }, + ], + }, + dropType: 'field_replace', + source: { + field: { + name: 'agent.keyword', + }, + indexPatternId: 'indexPattern1', + id: 'agent.keyword', + humanData: { + label: 'agent.keyword', + position: 2, + }, + }, + target: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'an1', + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + }).layers[0] + ).toEqual({ + layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', - annotations: [{ ...exampleAnnotation, id: 'an2' }], - ignoreGlobalFilters: true, - }, - ]); - }); - it('should swap the annotations between layers', () => { - expect( - xyVisualization.onDrop!({ - frame, - prevState: { - ...exampleState(), - layers: [ - { - layerId: 'first', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation], - ignoreGlobalFilters: true, + annotations: [ + { + filter: { + language: 'kuery', + query: 'agent.keyword: *', + type: 'kibana_query', }, - { - layerId: 'second', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation2], - ignoreGlobalFilters: true, + icon: 'circle', + id: 'an1', + key: { + type: 'point_in_time', }, - ], - }, - source: { - layerId: 'first', - groupId: 'xAnnotation', - columnId: 'an1', - id: 'an1', - humanData: { label: 'label' }, - filterOperations: () => true, - }, - target: { - layerId: 'second', - groupId: 'xAnnotation', - columnId: 'an2', - filterOperations: () => true, - }, - dropType: 'swap_compatible', - }).layers - ).toEqual([ - { - layerId: 'first', + label: 'agent.keyword: *', + timeField: 'timestamp', + type: 'query', + }, + ], + ignoreGlobalFilters: true, + }); + }); + it('dragging operation: should copy previous column if passed and assign a new id', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, + }, + ], + }, + dropType: 'duplicate_compatible', + source: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'an2', + id: 'an2', + humanData: { label: 'an2' }, + indexPatternId: 'indexPattern1', + }, + target: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'newColId', + filterOperations: Boolean, + indexPatternId: 'indexPattern1', + }, + }).layers[0] + ).toEqual({ + layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation2], + annotations: [exampleAnnotation2, { ...exampleAnnotation2, id: 'newColId' }], ignoreGlobalFilters: true, - }, - { - layerId: 'second', + }); + }); + it('dragging operation: should reorder a dimension to a annotation layer', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation, exampleAnnotation2], + ignoreGlobalFilters: true, + }, + ], + }, + source: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'an2', + id: 'an2', + humanData: { label: 'label' }, + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + target: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'an1', + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + dropType: 'reorder', + }).layers[0] + ).toEqual({ + layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation], + annotations: [exampleAnnotation2, exampleAnnotation], ignoreGlobalFilters: true, - }, - ]); - }); - it('should replace the target in another annotation layer', () => { - expect( - xyVisualization.onDrop!({ - frame, - prevState: { - ...exampleState(), - layers: [ - { - layerId: 'first', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation], - ignoreGlobalFilters: true, - }, - { - layerId: 'second', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation2], - ignoreGlobalFilters: true, - }, - ], + }); + }); + + it('dragging operation: should duplicate the annotations and replace the target in another annotation layer', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + dropType: 'replace_duplicate_compatible', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, }, - source: { + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [{ ...exampleAnnotation, id: 'an2' }], + ignoreGlobalFilters: true, + }, + ]); + }); + it('dragging operation: should swap the annotations between layers', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + dropType: 'swap_compatible', + }).layers + ).toEqual([ + { layerId: 'first', - groupId: 'xAnnotation', - columnId: 'an1', - id: 'an1', - humanData: { label: 'label' }, - filterOperations: () => true, + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, }, - target: { + { layerId: 'second', - groupId: 'xAnnotation', - columnId: 'an2', - filterOperations: () => true, + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, }, - dropType: 'replace_compatible', - }).layers - ).toEqual([ - { - layerId: 'first', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [], - ignoreGlobalFilters: true, - }, - { - layerId: 'second', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation], - ignoreGlobalFilters: true, - }, - ]); - }); - it('should move compatible to another annotation layer', () => { - expect( - xyVisualization.onDrop!({ - frame, - prevState: { - ...exampleState(), - layers: [ - { - layerId: 'first', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation], - ignoreGlobalFilters: true, - }, - { - layerId: 'second', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [], - ignoreGlobalFilters: true, - }, - ], + ]); + }); + it('dragging operation: should replace the target in another annotation layer', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + dropType: 'replace_compatible', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [], + ignoreGlobalFilters: true, }, - source: { + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, + }, + ]); + }); + it('dragging operation: should move compatible to another annotation layer', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [], + ignoreGlobalFilters: true, + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + indexPatternId: 'indexPattern1', + }, + dropType: 'move_compatible', + }).layers + ).toEqual([ + { layerId: 'first', - groupId: 'xAnnotation', - columnId: 'an1', - id: 'an1', - humanData: { label: 'label' }, - filterOperations: () => true, + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [], + ignoreGlobalFilters: true, }, - target: { + { layerId: 'second', - groupId: 'xAnnotation', - columnId: 'an2', - filterOperations: () => true, + layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', + annotations: [exampleAnnotation], + ignoreGlobalFilters: true, }, - dropType: 'move_compatible', - }).layers - ).toEqual([ - { - layerId: 'first', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [], - ignoreGlobalFilters: true, - }, - { - layerId: 'second', - layerType: layerTypes.ANNOTATIONS, - indexPatternId: 'indexPattern1', - annotations: [exampleAnnotation], - ignoreGlobalFilters: true, - }, - ]); + ]); + }); }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 34ab6c88ffa19..6184e3c607a82 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -20,12 +20,17 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { generateId } from '../../id_generator'; -import { renewIDs } from '../../utils'; +import { + isDraggedField, + isOperationFromCompatibleGroup, + isOperationFromTheSameGroup, + renewIDs, +} from '../../utils'; import { getSuggestions } from './xy_suggestions'; import { XyToolbar } from './xy_config_panel'; import { DimensionEditor } from './xy_config_panel/dimension_editor'; import { LayerHeader, LayerHeaderContent } from './xy_config_panel/layer_header'; -import type { Visualization, AccessorConfig, FramePublicAPI } from '../../types'; +import { Visualization, AccessorConfig, FramePublicAPI } from '../../types'; import { State, visualizationTypes, @@ -366,6 +371,39 @@ export const getXyVisualization = ({ return getFirstDataLayer(state.layers)?.palette; }, + getDropProps(dropProps) { + if (!dropProps.source) { + return; + } + const srcDataView = dropProps.source.indexPatternId; + const targetDataView = dropProps.target.indexPatternId; + if (!targetDataView || srcDataView !== targetDataView) { + return; + } + + if (isDraggedField(dropProps.source)) { + if (dropProps.source.field.type === 'document') { + return; + } + return dropProps.target.isNewColumn + ? { dropTypes: ['field_add'] } + : { dropTypes: ['field_replace'] }; + } + + if (isOperationFromTheSameGroup(dropProps.source, dropProps.target)) { + return dropProps.target.isNewColumn + ? { dropTypes: ['duplicate_compatible'] } + : { dropTypes: ['reorder'] }; + } + if (isOperationFromCompatibleGroup(dropProps.source, dropProps.target)) { + return { + dropTypes: dropProps.target.isNewColumn + ? ['move_compatible', 'duplicate_compatible'] + : ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], + }; + } + }, + onDrop(props) { const targetLayer: XYLayerConfig | undefined = props.prevState.layers.find( (l) => l.layerId === props.target.layerId @@ -724,6 +762,9 @@ export const getXyVisualization = ({ getUniqueLabels(state) { return getUniqueLabels(state.layers); }, + getUsedDataView(state, layerId) { + return getAnnotationsLayers(state.layers).find((l) => l.layerId === layerId)?.indexPatternId; + }, getUsedDataViews(state) { return ( state?.layers.filter(isAnnotationsLayer).map(({ indexPatternId }) => indexPatternId) ?? []