From e7c69ba3283f10b675613abb615953f5f9866a82 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 17 Jun 2022 17:27:08 +0200 Subject: [PATCH] added dnd between layers --- test/functional/services/common/browser.ts | 14 +- .../lens/public/drag_drop/drag_drop.test.tsx | 61 +- .../drag_drop/providers/announcements.tsx | 309 ++- .../lens/public/drag_drop/providers/types.tsx | 1 + .../buttons/draggable_dimension_button.tsx | 53 +- .../buttons/drop_targets_utils.test.tsx | 123 + .../buttons/drop_targets_utils.tsx | 109 +- .../buttons/empty_dimension_button.tsx | 64 +- .../config_panel/layer_panel.test.tsx | 112 +- .../editor_frame/config_panel/layer_panel.tsx | 161 +- .../editor_frame/config_panel/types.ts | 1 - .../droppable/droppable.test.ts | 2332 ----------------- .../droppable/get_drop_props.test.ts | 767 ++++++ .../droppable/get_drop_props.ts | 212 +- .../dimension_panel/droppable/mocks.ts | 292 +++ .../droppable/on_drop_handler.test.ts | 2257 ++++++++++++++++ .../droppable/on_drop_handler.ts | 650 +++-- .../dimension_panel/operation_support.ts | 6 +- .../definitions/formula/formula.tsx | 24 +- .../operations/definitions/formula/math.tsx | 4 +- .../operations/definitions/index.ts | 18 +- .../operations/definitions/static_value.tsx | 21 +- .../operations/layer_helpers.test.ts | 40 +- .../operations/layer_helpers.ts | 159 +- .../operations/time_scale_utils.ts | 2 +- .../indexpattern_datasource/state_helpers.ts | 16 + .../public/indexpattern_datasource/types.ts | 6 + x-pack/plugins/lens/public/types.ts | 46 +- .../xy_visualization/annotations/helpers.tsx | 250 +- .../reference_line_helpers.tsx | 6 +- .../xy_visualization/visualization.test.ts | 234 +- .../public/xy_visualization/visualization.tsx | 18 +- .../visualization_helpers.tsx | 5 +- .../translations/translations/fr-FR.json | 34 - .../translations/translations/ja-JP.json | 29 - .../translations/translations/zh-CN.json | 34 - .../test/functional/apps/lens/group1/table.ts | 8 +- .../apps/lens/group3/annotations.ts | 8 +- .../apps/lens/group3/drag_and_drop.ts | 166 +- .../apps/lens/group3/error_handling.ts | 8 +- .../functional/apps/lens/group3/formula.ts | 31 +- .../apps/lens/group3/reference_lines.ts | 16 +- .../test/functional/page_objects/lens_page.ts | 41 +- 43 files changed, 5370 insertions(+), 3378 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx delete mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 9bbe04b97fcfa..e5d13443d7658 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -258,7 +258,7 @@ class BrowserService extends FtrService { * @param {string} to html selector * @return {Promise} */ - public async html5DragAndDrop(from: string, to: string) { + public async html5DragAndDrop(from: string, to: string, through?: string) { await this.execute( ` function createEvent(typeOfEvent) { @@ -291,6 +291,15 @@ class BrowserService extends FtrService { const dragStartEvent = createEvent('dragstart'); dispatchEvent(origin, dragStartEvent); + const through = arguments[2] && document.querySelector(arguments[2]); + if (through){ + setTimeout(() => { + const dragOverEvent = createEvent('dragover'); + dispatchEvent(through, dragOverEvent, dragStartEvent.dataTransfer); + }, 50); + } + + setTimeout(() => { const dropEvent = createEvent('drop'); const target = document.querySelector(arguments[1]); @@ -300,7 +309,8 @@ class BrowserService extends FtrService { }, 100); `, from, - to + to, + through ); // wait for 150ms to make sure the script has run await setTimeoutAsync(150); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index 1d6c14c09136a..c0d7766fc22d8 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -41,7 +41,14 @@ describe('DragDrop', () => { const value = { id: '1', - humanData: { label: 'hello', groupLabel: 'X', position: 1, canSwap: true, canDuplicate: true }, + humanData: { + label: 'hello', + groupLabel: 'X', + position: 1, + canSwap: true, + canDuplicate: true, + layerNumber: 0, + }, }; test('renders if nothing is being dragged', () => { @@ -205,7 +212,7 @@ describe('DragDrop', () => { order={[2, 0, 1, 0]} onDrop={(x: unknown) => {}} dropTypes={undefined} - value={{ id: '2', humanData: { label: 'label2' } }} + value={{ id: '2', humanData: { label: 'label2', layerNumber: 0 } }} > @@ -231,7 +238,7 @@ describe('DragDrop', () => { }} > @@ -286,7 +293,7 @@ describe('DragDrop', () => { registerDropTarget={jest.fn()} > @@ -329,7 +336,7 @@ describe('DragDrop', () => { draggable: true, value: { id: '1', - humanData: { label: 'Label1', position: 1 }, + humanData: { label: 'Label1', position: 1, layerNumber: 0 }, }, children: '1', order: [2, 0, 0, 0], @@ -341,7 +348,7 @@ describe('DragDrop', () => { value: { id: '2', - humanData: { label: 'label2', position: 1 }, + humanData: { label: 'label2', position: 1, layerNumber: 0 }, }, onDrop, dropTypes: ['move_compatible'] as DropType[], @@ -358,6 +365,7 @@ describe('DragDrop', () => { groupLabel: 'Y', canSwap: true, canDuplicate: true, + layerNumber: 0, }, }, onDrop, @@ -373,7 +381,7 @@ describe('DragDrop', () => { dragType: 'move' as 'copy' | 'move', value: { id: '4', - humanData: { label: 'label4', position: 2, groupLabel: 'Y' }, + humanData: { label: 'label4', position: 2, groupLabel: 'Y', layerNumber: 0 }, }, order: [2, 0, 2, 1], }, @@ -415,11 +423,11 @@ describe('DragDrop', () => { }); keyboardHandler.simulate('keydown', { key: 'Enter' }); expect(setA11yMessage).toBeCalledWith( - `You're dragging Label1 from at position 1 over label3 from Y group at position 1. Press space or enter to replace label3 with Label1. Hold alt or option to duplicate. Hold shift to swap.` + `You're dragging Label1 from at position 1 in layer 0 over label3 from Y group at position 1 in layer 0. Press space or enter to replace label3 with Label1. Hold alt or option to duplicate. Hold shift to swap.` ); expect(setActiveDropTarget).toBeCalledWith(undefined); expect(onDrop).toBeCalledWith( - { humanData: { label: 'Label1', position: 1 }, id: '1' }, + { humanData: { label: 'Label1', position: 1, layerNumber: 0 }, id: '1' }, 'move_compatible' ); }); @@ -474,7 +482,7 @@ describe('DragDrop', () => { draggable: true, value: { id: '1', - humanData: { label: 'Label1', position: 1 }, + humanData: { label: 'Label1', position: 1, layerNumber: 0 }, }, children: '1', order: [2, 0, 0, 0], @@ -486,7 +494,7 @@ describe('DragDrop', () => { value: { id: '2', - humanData: { label: 'label2', position: 1 }, + humanData: { label: 'label2', position: 1, layerNumber: 0 }, }, onDrop, dropTypes: ['move_compatible'] as DropType[], @@ -533,7 +541,7 @@ describe('DragDrop', () => { component = mount( { registerDropTarget={jest.fn()} > @@ -629,18 +637,24 @@ describe('DragDrop', () => { component.find('SingleDropInner').at(0).simulate('dragover'); component.find('SingleDropInner').at(0).simulate('drop'); - expect(onDrop).toBeCalledWith({ humanData: { label: 'Label1' }, id: '1' }, 'move_compatible'); + expect(onDrop).toBeCalledWith( + { humanData: { label: 'Label1', layerNumber: 0 }, id: '1' }, + 'move_compatible' + ); component.find('SingleDropInner').at(1).simulate('dragover'); component.find('SingleDropInner').at(1).simulate('drop'); expect(onDrop).toBeCalledWith( - { humanData: { label: 'Label1' }, id: '1' }, + { humanData: { label: 'Label1', layerNumber: 0 }, id: '1' }, 'duplicate_compatible' ); component.find('SingleDropInner').at(2).simulate('dragover'); component.find('SingleDropInner').at(2).simulate('drop'); - expect(onDrop).toBeCalledWith({ humanData: { label: 'Label1' }, id: '1' }, 'swap_compatible'); + expect(onDrop).toBeCalledWith( + { humanData: { label: 'Label1', layerNumber: 0 }, id: '1' }, + 'swap_compatible' + ); }); test('pressing Alt or Shift when dragging over the main drop target sets extra drop target as active', () => { @@ -693,7 +707,7 @@ describe('DragDrop', () => { draggable: true, value: { id: '1', - humanData: { label: 'Label1', position: 1 }, + humanData: { label: 'Label1', position: 1, layerNumber: 0 }, }, children: '1', order: [2, 0, 0, 0], @@ -705,7 +719,7 @@ describe('DragDrop', () => { value: { id: '2', - humanData: { label: 'label2', position: 1 }, + humanData: { label: 'label2', position: 1, layerNumber: 0 }, }, onDrop, dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'] as DropType[], @@ -716,7 +730,7 @@ describe('DragDrop', () => { dragType: 'move' as const, value: { id: '3', - humanData: { label: 'label3', position: 1, groupLabel: 'Y' }, + humanData: { label: 'label3', position: 1, groupLabel: 'Y', layerNumber: 0 }, }, onDrop, dropTypes: ['replace_compatible'] as DropType[], @@ -734,6 +748,7 @@ describe('DragDrop', () => { humanData: { label: 'label2', position: 1, + layerNumber: 0, }, id: '2', onDrop, @@ -743,6 +758,7 @@ describe('DragDrop', () => { humanData: { label: 'label2', position: 1, + layerNumber: 0, }, id: '2', onDrop, @@ -753,6 +769,7 @@ describe('DragDrop', () => { groupLabel: 'Y', label: 'label3', position: 1, + layerNumber: 0, }, id: '3', onDrop, @@ -942,18 +959,18 @@ describe('DragDrop', () => { const items = [ { id: '1', - humanData: { label: 'Label1', position: 1, groupLabel: 'X' }, + humanData: { label: 'Label1', position: 1, groupLabel: 'X', layerNumber: 0 }, onDrop, draggable: true, }, { id: '2', - humanData: { label: 'label2', position: 2, groupLabel: 'X' }, + humanData: { label: 'label2', position: 2, groupLabel: 'X', layerNumber: 0 }, onDrop, }, { id: '3', - humanData: { label: 'label3', position: 3, groupLabel: 'X' }, + humanData: { label: 'label3', position: 3, groupLabel: 'X', layerNumber: 0 }, onDrop, }, ]; diff --git a/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx index 0d247825b6a17..a21656421a96f 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx @@ -22,19 +22,21 @@ interface CustomAnnouncementsType { const replaceAnnouncement = { selectedTarget: ( - { label, groupLabel, position }: HumanData, + { label, groupLabel, position, layerNumber }: HumanData, { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, canSwap, canDuplicate, + canCombine, + layerNumber: dropLayerNumber, }: HumanData, announceModifierKeys?: boolean ) => { if (announceModifierKeys && (canSwap || canDuplicate)) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over {dropLabel} from {dropGroupLabel} group at position {dropPosition}. Press space or enter to replace {dropLabel} with {label}.{duplicateCopy}{swapCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to replace {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { label, groupLabel, @@ -44,63 +46,76 @@ const replaceAnnouncement = { dropPosition, duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', swapCopy: canSwap ? SWAP_SHORT : '', + combineCopy: canCombine ? COMBINE_SHORT : '', + layerNumber, + dropLayerNumber, }, }); } - return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replace', { - defaultMessage: `Replace {dropLabel} in {dropGroupLabel} group at position {dropPosition} with {label}. Press space or enter to replace.`, + defaultMessage: `Replace {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber} with {label}. Press space or enter to replace.`, values: { label, dropLabel, dropGroupLabel, dropPosition, + dropLayerNumber, }, }); }, - dropped: ({ label }: HumanData, { label: dropLabel, groupLabel, position }: HumanData) => - i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', { - defaultMessage: 'Replaced {dropLabel} with {label} in {groupLabel} at position {position}', + dropped: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position, layerNumber: dropLayerNumber }: HumanData + ) => { + return i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', { + defaultMessage: + 'Replaced {dropLabel} with {label} in {groupLabel} at position {position} in layer {dropLayerNumber}', values: { label, dropLabel, groupLabel, position, + dropLayerNumber, }, - }), + }); + }, }; const duplicateAnnouncement = { selectedTarget: ( - { label, groupLabel }: HumanData, + { label, groupLabel, layerNumber }: HumanData, { groupLabel: dropGroupLabel, position }: HumanData ) => { if (groupLabel !== dropGroupLabel) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicated', { - defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate`, + defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position} in layer {layerNumber}. Hold Alt or Option and press space or enter to duplicate`, values: { label, dropGroupLabel, position, + layerNumber, }, }); } return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup', { - defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position}. Press space or enter to duplicate`, + defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position} in layer {layerNumber}. Press space or enter to duplicate`, values: { label, dropGroupLabel, position, + layerNumber, }, }); }, - dropped: ({ label }: HumanData, { groupLabel, position }: HumanData) => + dropped: ({ label }: HumanData, { groupLabel, position, layerNumber }: HumanData) => i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicated', { - defaultMessage: 'Duplicated {label} in {groupLabel} group at position {position}', + defaultMessage: + 'Duplicated {label} in {groupLabel} group at position {position} in layer {layerNumber}', values: { label, groupLabel, position, + layerNumber, }, }), }; @@ -109,8 +124,8 @@ const reorderAnnouncement = { selectedTarget: ( { label, groupLabel, position: prevPosition }: HumanData, { position }: HumanData - ) => - prevPosition === position + ) => { + return prevPosition === position ? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reorderedBack', { defaultMessage: `{label} returned to its initial position {prevPosition}`, values: { @@ -121,12 +136,13 @@ const reorderAnnouncement = { : i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reordered', { defaultMessage: `Reorder {label} in {groupLabel} group from position {prevPosition} to position {position}. Press space or enter to reorder`, values: { - groupLabel, label, + groupLabel, position, prevPosition, }, - }), + }); + }, dropped: ({ label, groupLabel, position: prevPosition }: HumanData, { position }: HumanData) => i18n.translate('xpack.lens.dragDrop.announce.dropped.reordered', { defaultMessage: @@ -142,7 +158,7 @@ const reorderAnnouncement = { const combineAnnouncement = { selectedTarget: ( - { label, groupLabel, position }: HumanData, + { label, groupLabel, position, layerNumber }: HumanData, { label: dropLabel, groupLabel: dropGroupLabel, @@ -150,12 +166,13 @@ const combineAnnouncement = { canSwap, canDuplicate, canCombine, + layerNumber: dropLayerNumber, }: HumanData, announceModifierKeys?: boolean ) => { if (announceModifierKeys && (canSwap || canDuplicate || canCombine)) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.combineMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over {dropLabel} from {dropGroupLabel} group at position {dropPosition}. Press space or enter to combine {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to combine {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { label, groupLabel, @@ -166,28 +183,35 @@ const combineAnnouncement = { duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', swapCopy: canSwap ? SWAP_SHORT : '', combineCopy: canCombine ? COMBINE_SHORT : '', + layerNumber, + dropLayerNumber, }, }); } - return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.combine', { - defaultMessage: `Combine {dropLabel} in {dropGroupLabel} group at position {dropPosition} with {label}. Press space or enter to combine.`, + defaultMessage: `Combine {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber} with {label}. Press space or enter to combine.`, values: { label, dropLabel, dropGroupLabel, dropPosition, + dropLayerNumber, }, }); }, - dropped: ({ label }: HumanData, { label: dropLabel, groupLabel, position }: HumanData) => + dropped: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position, layerNumber: dropLayerNumber }: HumanData + ) => i18n.translate('xpack.lens.dragDrop.announce.duplicated.combine', { - defaultMessage: 'Combine {dropLabel} with {label} in {groupLabel} at position {position}', + defaultMessage: + 'Combine {dropLabel} with {label} in {groupLabel} at position {position} in layer {dropLayerNumber}', values: { label, dropLabel, groupLabel, position, + dropLayerNumber, }, }), }; @@ -212,7 +236,7 @@ export const announcements: CustomAnnouncementsType = { field_combine: combineAnnouncement.selectedTarget, replace_compatible: replaceAnnouncement.selectedTarget, replace_incompatible: ( - { label, groupLabel, position }: HumanData, + { label, groupLabel, position, layerNumber }: HumanData, { label: dropLabel, groupLabel: dropGroupLabel, @@ -220,14 +244,16 @@ export const announcements: CustomAnnouncementsType = { nextLabel, canSwap, canDuplicate, + canCombine, + layerNumber: dropLayerNumber, }: HumanData, announceModifierKeys?: boolean ) => { - if (announceModifierKeys && (canSwap || canDuplicate)) { + if (announceModifierKeys && (canSwap || canDuplicate || canCombine)) { return i18n.translate( 'xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over {dropLabel} from {dropGroupLabel} group at position {dropPosition}. Press space or enter to convert {label} to {nextLabel} and replace {dropLabel}.{duplicateCopy}{swapCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to convert {label} to {nextLabel} and replace {dropLabel}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { label, groupLabel, @@ -238,35 +264,40 @@ export const announcements: CustomAnnouncementsType = { nextLabel, duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', swapCopy: canSwap ? SWAP_SHORT : '', + combineCopy: canCombine ? COMBINE_SHORT : '', + layerNumber, + dropLayerNumber, }, } ); } return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible', { - defaultMessage: `Convert {label} to {nextLabel} and replace {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Press space or enter to replace`, + defaultMessage: `Convert {label} to {nextLabel} and replace {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to replace`, values: { label, - nextLabel, dropLabel, dropGroupLabel, dropPosition, + nextLabel, + dropLayerNumber, }, }); }, move_incompatible: ( - { label, groupLabel, position }: HumanData, + { label, groupLabel, position, layerNumber }: HumanData, { groupLabel: dropGroupLabel, position: dropPosition, nextLabel, canSwap, canDuplicate, + layerNumber: dropLayerNumber, }: HumanData, announceModifierKeys?: boolean ) => { if (announceModifierKeys && (canSwap || canDuplicate)) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group. Press space or enter to convert {label} to {nextLabel} and move.{duplicateCopy}{swapCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over position {dropPosition} in {dropGroupLabel} group in layer {dropLayerNumber}. Press space or enter to convert {label} to {nextLabel} and move.{duplicateCopy}`, values: { label, groupLabel, @@ -275,29 +306,37 @@ export const announcements: CustomAnnouncementsType = { dropPosition, nextLabel, duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', - swapCopy: canSwap ? SWAP_SHORT : '', + layerNumber, + dropLayerNumber, }, }); } return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible', { - defaultMessage: `Convert {label} to {nextLabel} and move to {dropGroupLabel} group at position {dropPosition}. Press space or enter to move`, + defaultMessage: `Convert {label} to {nextLabel} and move to {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to move`, values: { label, - nextLabel, dropGroupLabel, dropPosition, + nextLabel, + dropLayerNumber, }, }); }, move_compatible: ( { label, groupLabel, position }: HumanData, - { groupLabel: dropGroupLabel, position: dropPosition, canSwap, canDuplicate }: HumanData, + { + groupLabel: dropGroupLabel, + position: dropPosition, + canSwap, + canDuplicate, + layerNumber: dropLayerNumber, + }: HumanData, announceModifierKeys?: boolean ) => { if (announceModifierKeys && (canSwap || canDuplicate)) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group. Press space or enter to move.{duplicateCopy}{swapCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group in layer {dropLayerNumber}. Press space or enter to move.{duplicateCopy}`, values: { label, groupLabel, @@ -305,69 +344,78 @@ export const announcements: CustomAnnouncementsType = { dropGroupLabel, dropPosition, duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', - swapCopy: canSwap ? SWAP_SHORT : '', + dropLayerNumber, }, }); } return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatible', { - defaultMessage: `Move {label} to {dropGroupLabel} group at position {dropPosition}. Press space or enter to move`, + defaultMessage: `Move {label} to {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to move`, values: { label, dropGroupLabel, dropPosition, + dropLayerNumber, }, }); }, duplicate_incompatible: ( { label }: HumanData, - { groupLabel, position, nextLabel }: HumanData + { groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible', { defaultMessage: - 'Convert copy of {label} to {nextLabel} and add to {groupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate', + 'Convert copy of {label} to {nextLabel} and add to {groupLabel} group at position {position} in layer {dropLayerNumber}. Hold Alt or Option and press space or enter to duplicate', values: { label, groupLabel, position, nextLabel, + dropLayerNumber, }, }), replace_duplicate_incompatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position, nextLabel }: HumanData + { label: dropLabel, groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible', { defaultMessage: - 'Convert copy of {label} to {nextLabel} and replace {dropLabel} in {groupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate and replace', + 'Convert copy of {label} to {nextLabel} and replace {dropLabel} in {groupLabel} group at position {position} in layer {dropLayerNumber}. Hold Alt or Option and press space or enter to duplicate and replace', values: { label, groupLabel, position, dropLabel, nextLabel, + dropLayerNumber, }, }), replace_duplicate_compatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position }: HumanData + { label: dropLabel, groupLabel, position, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible', { defaultMessage: - 'Duplicate {label} and replace {dropLabel} in {groupLabel} at position {position}. Hold Alt or Option and press space or enter to duplicate and replace', + 'Duplicate {label} and replace {dropLabel} in {groupLabel} at position {position} in layer {dropLayerNumber}. Hold Alt or Option and press space or enter to duplicate and replace', values: { label, dropLabel, groupLabel, position, + dropLayerNumber, }, }), swap_compatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.swapCompatible', { defaultMessage: - 'Swap {label} in {groupLabel} group at position {position} with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Shift and press space or enter to swap', + 'Swap {label} in {groupLabel} group at position {position} in layer {layerNumber} with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Hold Shift and press space or enter to swap', values: { label, groupLabel, @@ -375,15 +423,23 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropGroupLabel, dropPosition, + layerNumber, + dropLayerNumber, }, }), swap_incompatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible', { defaultMessage: - 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} and swap with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Shift and press space or enter to swap', + 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} in layer {layerNumber} and swap with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Hold Shift and press space or enter to swap', values: { label, groupLabel, @@ -392,15 +448,22 @@ export const announcements: CustomAnnouncementsType = { dropGroupLabel, dropPosition, nextLabel, + layerNumber, + dropLayerNumber, }, }), combine_compatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.combineCompatible', { defaultMessage: - 'Combine {label} in {groupLabel} group at position {position} with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Control and press space or enter to combine', + 'Combine {label} in {groupLabel} group at position {position} in layer {layerNumber} with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Hold Control and press space or enter to combine', values: { label, groupLabel, @@ -408,15 +471,23 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropGroupLabel, dropPosition, + layerNumber, + dropLayerNumber, }, }), combine_incompatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.combineIncompatible', { defaultMessage: - 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} and combine with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Control and press space or enter to combine', + 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} in layer {layerNumber} and combine with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Hold Control and press space or enter to combine', values: { label, groupLabel, @@ -425,6 +496,8 @@ export const announcements: CustomAnnouncementsType = { dropGroupLabel, dropPosition, nextLabel, + dropLayerNumber, + layerNumber, }, }), }, @@ -436,92 +509,110 @@ export const announcements: CustomAnnouncementsType = { replace_compatible: replaceAnnouncement.dropped, replace_incompatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position, nextLabel }: HumanData + { label: dropLabel, groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.replaceIncompatible', { defaultMessage: - 'Converted {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position}', + 'Converted {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, nextLabel, dropLabel, groupLabel, position, + dropLayerNumber, }, }), - move_incompatible: ({ label }: HumanData, { groupLabel, position, nextLabel }: HumanData) => + move_incompatible: ( + { label }: HumanData, + { groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData + ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.moveIncompatible', { defaultMessage: - 'Converted {label} to {nextLabel} and moved to {groupLabel} group at position {position}', + 'Converted {label} to {nextLabel} and moved to {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, nextLabel, groupLabel, position, + dropLayerNumber, }, }), - move_compatible: ({ label }: HumanData, { groupLabel, position }: HumanData) => + move_compatible: ( + { label }: HumanData, + { groupLabel, position, layerNumber: dropLayerNumber }: HumanData + ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.moveCompatible', { - defaultMessage: 'Moved {label} to {groupLabel} group at position {position}', + defaultMessage: + 'Moved {label} to {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, groupLabel, position, + dropLayerNumber, }, }), duplicate_incompatible: ( { label }: HumanData, - { groupLabel, position, nextLabel }: HumanData + { groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicateIncompatible', { defaultMessage: - 'Converted copy of {label} to {nextLabel} and added to {groupLabel} group at position {position}', + 'Converted copy of {label} to {nextLabel} and added to {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, groupLabel, position, nextLabel, + dropLayerNumber, }, }), replace_duplicate_incompatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position, nextLabel }: HumanData + { label: dropLabel, groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible', { defaultMessage: - 'Converted copy of {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position}', + 'Converted copy of {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, dropLabel, groupLabel, position, nextLabel, + dropLayerNumber, }, }), replace_duplicate_compatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position }: HumanData + { label: dropLabel, groupLabel, position, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible', { defaultMessage: - 'Replaced {dropLabel} with a copy of {label} in {groupLabel} at position {position}', + 'Replaced {dropLabel} with a copy of {label} in {groupLabel} at position {position} in layer {dropLayerNumber}', values: { label, dropLabel, groupLabel, position, + dropLayerNumber, }, }), swap_compatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.swapCompatible', { defaultMessage: - 'Moved {label} to {dropGroupLabel} at position {dropPosition} and {dropLabel} to {groupLabel} group at position {position}', + 'Moved {label} to {dropGroupLabel} at position {dropPosition} in layer {dropLayerNumber} and {dropLabel} to {groupLabel} group at position {position} in layer {layerNumber}', values: { label, groupLabel, @@ -529,15 +620,23 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropGroupLabel, dropPosition, + layerNumber, + dropLayerNumber, }, }), swap_incompatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.swapIncompatible', { defaultMessage: - 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} and swapped with {dropLabel} in {dropGroupLabel} group at position {dropPosition}', + 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} in layer {layerNumber} and swapped with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}', values: { label, groupLabel, @@ -546,31 +645,44 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropPosition, nextLabel, + dropLayerNumber, + layerNumber, }, }), combine_compatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition }: HumanData + { label, groupLabel }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.combineCompatible', { defaultMessage: - 'Combined {label} to {dropGroupLabel} at position {dropPosition} and {dropLabel} to {groupLabel} group at position {position}', + 'Combined {label} in group {groupLabel} to {dropLabel} in group {dropGroupLabel} at position {dropPosition} in layer {dropLayerNumber}', values: { label, groupLabel, - position, dropLabel, dropGroupLabel, dropPosition, + dropLayerNumber, }, }), combine_incompatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.combineIncompatible', { defaultMessage: - 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} and combined with {dropLabel} in {dropGroupLabel} group at position {dropPosition}', + 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} and combined with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}', values: { label, groupLabel, @@ -579,6 +691,7 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropPosition, nextLabel, + dropLayerNumber, }, }), }, @@ -620,15 +733,22 @@ const defaultAnnouncements = { dropped: ( { label }: HumanData, - { groupLabel: dropGroupLabel, position, label: dropLabel }: HumanData + { + groupLabel: dropGroupLabel, + position, + label: dropLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => dropGroupLabel && position ? i18n.translate('xpack.lens.dragDrop.announce.droppedDefault', { - defaultMessage: 'Added {label} in {dropGroupLabel} group at position {position}', + defaultMessage: + 'Added {label} in {dropGroupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, dropGroupLabel, position, + dropLayerNumber, }, }) : i18n.translate('xpack.lens.dragDrop.announce.droppedNoPosition', { @@ -640,15 +760,21 @@ const defaultAnnouncements = { }), selectedTarget: ( { label }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position }: HumanData + { + label: dropLabel, + groupLabel: dropGroupLabel, + position, + layerNumber: dropLayerNumber, + }: HumanData ) => { return dropGroupLabel && position ? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.default', { - defaultMessage: `Add {label} to {dropGroupLabel} group at position {position}. Press space or enter to add`, + defaultMessage: `Add {label} to {dropGroupLabel} group at position {position} in layer {dropLayerNumber}. Press space or enter to add`, values: { label, dropGroupLabel, position, + dropLayerNumber, }, }) : i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition', { @@ -671,8 +797,15 @@ export const announce = { dropElement: HumanData, type?: DropType, announceModifierKeys?: boolean - ) => - (type && - announcements.selectedTarget?.[type]?.(draggedElement, dropElement, announceModifierKeys)) || - defaultAnnouncements.selectedTarget(draggedElement, dropElement), + ) => { + return ( + (type && + announcements.selectedTarget?.[type]?.( + draggedElement, + dropElement, + announceModifierKeys + )) || + defaultAnnouncements.selectedTarget(draggedElement, dropElement) + ); + }, }; diff --git a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx index 921ab897706c0..363f0b41ef3a1 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx @@ -10,6 +10,7 @@ import { DropType } from '../../types'; export interface HumanData { label: string; groupLabel?: string; + layerNumber?: number; position?: number; nextLabel?: string; canSwap?: boolean; 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 f0e0911b708fd..32aba270e846b 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 @@ -10,10 +10,10 @@ import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop import { Datasource, VisualizationDimensionGroupConfig, - isDraggedOperation, + isOperation, DropType, + DatasourceLayers, } from '../../../../types'; -import { LayerDatasourceDropProps } from '../types'; import { getCustomDropTarget, getAdditionalClassesOnDroppable, @@ -29,45 +29,53 @@ export function DraggableDimensionButton({ layerIndex, columnId, group, - groups, onDrop, onDragStart, onDragEnd, children, - layerDatasourceDropProps, + state, layerDatasource, + datasourceLayers, registerNewButtonRef, }: { layerId: string; groupIndex: number; layerIndex: number; - onDrop: ( - droppedItem: DragDropIdentifier, - dropTarget: DragDropIdentifier, - dropType?: DropType - ) => void; + onDrop: (source: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType) => void; onDragStart: () => void; onDragEnd: () => void; group: VisualizationDimensionGroupConfig; - groups: VisualizationDimensionGroupConfig[]; label: string; children: ReactElement; layerDatasource: Datasource; - layerDatasourceDropProps: LayerDatasourceDropProps; + datasourceLayers: DatasourceLayers; + state: unknown; accessorIndex: number; columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; }) { const { dragging } = useContext(DragContext); - const dropProps = getDropProps(layerDatasource, { - ...(layerDatasourceDropProps || {}), - dragging, - columnId, - filterOperations: group.filterOperations, - groupId: group.groupId, - dimensionGroups: groups, - }); + const sharedDatasource = + !isOperation(dragging) || + datasourceLayers?.[dragging.layerId]?.datasourceId === datasourceLayers?.[layerId]?.datasourceId + ? layerDatasource + : undefined; + + const dropProps = getDropProps( + { + state, + source: dragging, + target: { + layerId, + columnId, + groupId: group.groupId, + filterOperations: group.filterOperations, + prioritizedOperation: group.prioritizedOperation, + }, + }, + sharedDatasource + ); const dropTypes = dropProps?.dropTypes; const nextLabel = dropProps?.nextLabel; @@ -104,6 +112,7 @@ export function DraggableDimensionButton({ groupLabel: group.groupLabel, position: accessorIndex + 1, nextLabel: nextLabel || '', + layerNumber: layerIndex + 1, }, }), [ @@ -118,10 +127,10 @@ export function DraggableDimensionButton({ canDuplicate, canSwap, canCombine, + layerIndex, ] ); - // todo: simplify by id and use drop targets? const reorderableGroup = useMemo( () => group.accessors.map((g) => ({ @@ -136,7 +145,7 @@ export function DraggableDimensionButton({ ); const handleOnDrop = useCallback( - (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + (source, selectedDropType) => onDrop(source, value, selectedDropType), [value, onDrop] ); return ( @@ -151,7 +160,7 @@ export function DraggableDimensionButton({ getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable} order={[2, layerIndex, groupIndex, accessorIndex]} draggable - dragType={isDraggedOperation(dragging) ? 'move' : 'copy'} + dragType={isOperation(dragging) ? 'move' : 'copy'} dropTypes={dropTypes} reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined} value={value} 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 new file mode 100644 index 0000000000000..dd5ec847fb5b5 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx @@ -0,0 +1,123 @@ +/* + * 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' }, + }, + }, + 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' }, + }, + }) + ).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' }, + }, + }) + ).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' }, + }, + }) + ).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' }, + }, + }) + ).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 056efbf379d8a..3fdc1d1b43550 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,8 +9,17 @@ import React from 'react'; import classNames from 'classnames'; import { EuiIcon, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DraggingIdentifier } from '../../../../drag_drop'; -import { Datasource, DropType, GetDropProps } from '../../../../types'; +import { DragDropIdentifier, DraggingIdentifier } from '../../../../drag_drop'; +import { + Datasource, + DropType, + FramePublicAPI, + GetDropProps, + isOperation, + Visualization, + DragDropOperation, + VisualizationDimensionGroupConfig, +} from '../../../../types'; function getPropsForDropType(type: 'swap' | 'duplicate' | 'combine') { switch (type) { @@ -131,35 +140,97 @@ export const getAdditionalClassesOnDroppable = (dropType?: string) => { } }; -const isOperationFromTheSameGroup = ( - op1?: DraggingIdentifier, - op2?: { layerId: string; groupId: string; columnId: string } -) => { +const isOperationFromCompatibleGroup = (op1?: DraggingIdentifier, op2?: DragDropOperation) => { return ( - op1 && - op2 && - 'columnId' in op1 && + 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 && - 'groupId' in op1 && op1.groupId === op2.groupId && - 'layerId' in op1 && op1.layerId === op2.layerId ); }; +export function getDropPropsForSameGroup( + isNewColumn?: boolean +): { dropTypes: DropType[]; nextLabel?: string } | undefined { + return !isNewColumn ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; +} + export const getDropProps = ( - layerDatasource: Datasource, dropProps: GetDropProps, - isNew?: boolean + sharedDatasource?: Datasource ): { dropTypes: DropType[]; nextLabel?: string } | undefined => { - if (layerDatasource) { - return layerDatasource.getDropProps(dropProps); + if (sharedDatasource) { + return sharedDatasource?.getDropProps(dropProps); } else { - // TODO: refactor & test this - it's too annotations specific - // TODO: allow moving operations between layers for annotations - if (isOperationFromTheSameGroup(dropProps.dragging, dropProps)) { - return { dropTypes: [isNew ? 'duplicate_compatible' : 'reorder'], nextLabel: '' }; + 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; + source: DragDropIdentifier; + frame: FramePublicAPI; + dropType: DropType; + group?: VisualizationDimensionGroupConfig; +} + +export function onDropForVisualization( + props: OnVisDropProps, + activeVisualization: Visualization +) { + const { prevState, target, frame, dropType, source, group } = props; + const { layerId, columnId, groupId } = target; + + const previousColumn = + isOperation(source) && group?.requiresPreviousColumnOnDuplicate ? source.columnId : undefined; + + const newVisState = activeVisualization.setDimension({ + columnId, + groupId, + layerId, + prevState, + previousColumn, + frame, + }); + + // remove source + if ( + isOperation(source) && + (dropType === 'move_compatible' || + dropType === 'move_incompatible' || + dropType === 'combine_incompatible' || + dropType === 'combine_compatible' || + dropType === 'replace_compatible' || + dropType === 'replace_incompatible') + ) { + return activeVisualization.removeDimension({ + columnId: source?.columnId, + layerId: source?.layerId, + prevState: newVisState, + frame, + }); + } + return newVisState; +} 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 867ce32ea700e..a35366611ae18 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 @@ -12,8 +12,13 @@ import { i18n } from '@kbn/i18n'; import { generateId } from '../../../../id_generator'; import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop'; -import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../../types'; -import { LayerDatasourceDropProps } from '../types'; +import { + Datasource, + VisualizationDimensionGroupConfig, + DropType, + DatasourceLayers, + isOperation, +} from '../../../../types'; import { getCustomDropTarget, getAdditionalClassesOnDroppable, @@ -98,31 +103,31 @@ const SuggestedValueButton = ({ columnId, group, onClick }: EmptyButtonProps) => export function EmptyDimensionButton({ group, - groups, layerDatasource, - layerDatasourceDropProps, + state, layerId, groupIndex, layerIndex, onClick, onDrop, + datasourceLayers, }: { layerId: string; groupIndex: number; layerIndex: number; - onDrop: ( - droppedItem: DragDropIdentifier, - dropTarget: DragDropIdentifier, - dropType?: DropType - ) => void; + onDrop: (source: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType) => void; onClick: (id: string) => void; group: VisualizationDimensionGroupConfig; - groups: VisualizationDimensionGroupConfig[]; - layerDatasource: Datasource; - layerDatasourceDropProps: LayerDatasourceDropProps; + datasourceLayers: DatasourceLayers; + state: unknown; }) { const { dragging } = useContext(DragContext); + const sharedDatasource = + !isOperation(dragging) || + datasourceLayers?.[dragging.layerId]?.datasourceId === datasourceLayers?.[layerId]?.datasourceId + ? layerDatasource + : undefined; const itemIndex = group.accessors.length; @@ -132,16 +137,19 @@ export function EmptyDimensionButton({ }, [itemIndex]); const dropProps = getDropProps( - layerDatasource, { - ...(layerDatasourceDropProps || {}), - dragging, - columnId: newColumnId, - filterOperations: group.filterOperations, - groupId: group.groupId, - dimensionGroups: groups, + state, + source: dragging, + target: { + layerId, + columnId: newColumnId, + groupId: group.groupId, + filterOperations: group.filterOperations, + prioritizedOperation: group.prioritizedOperation, + isNewColumn: true, + }, }, - true + sharedDatasource ); const dropTypes = dropProps?.dropTypes; @@ -157,6 +165,7 @@ export function EmptyDimensionButton({ columnId: newColumnId, groupId: group.groupId, layerId, + filterOperations: group.filterOperations, id: newColumnId, humanData: { label, @@ -164,13 +173,24 @@ export function EmptyDimensionButton({ position: itemIndex + 1, nextLabel: nextLabel || '', canDuplicate, + layerNumber: layerIndex + 1, }, }), - [newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel, canDuplicate] + [ + newColumnId, + group.groupId, + layerId, + group.groupLabel, + group.filterOperations, + itemIndex, + nextLabel, + canDuplicate, + layerIndex, + ] ); const handleOnDrop = React.useCallback( - (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + (source, selectedDropType) => onDrop(source, value, selectedDropType), [value, onDrop] ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index e5da3b0feef03..02c5f1c23967f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -631,7 +631,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragging: draggingField, + source: draggingField, }) ); @@ -644,7 +644,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - droppedItem: draggingField, + source: draggingField, }) ); }); @@ -663,8 +663,8 @@ describe('LayerPanel', () => { ], }); - mockDatasource.getDropProps.mockImplementation(({ columnId }) => - columnId !== 'a' ? { dropTypes: ['field_replace'], nextLabel: '' } : undefined + mockDatasource.getDropProps.mockImplementation(({ target }) => + target.columnId !== 'a' ? { dropTypes: ['field_replace'], nextLabel: '' } : undefined ); const { instance } = await mountWithProvider( @@ -674,7 +674,9 @@ describe('LayerPanel', () => { ); expect(mockDatasource.getDropProps).toHaveBeenCalledWith( - expect.objectContaining({ columnId: 'a' }) + expect.objectContaining({ + target: expect.objectContaining({ columnId: 'a', groupId: 'a', layerId: 'first' }), + }) ); expect( @@ -741,7 +743,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragging: draggingOperation, + source: draggingOperation, }) ); @@ -755,8 +757,8 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - columnId: 'b', - droppedItem: draggingOperation, + target: expect.objectContaining({ columnId: 'b' }), + source: draggingOperation, }) ); @@ -771,8 +773,8 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - columnId: 'newid', - droppedItem: draggingOperation, + target: expect.objectContaining({ columnId: 'newid' }), + source: draggingOperation, }) ); }); @@ -816,7 +818,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ dropType: 'reorder', - droppedItem: draggingOperation, + source: draggingOperation, }) ); const secondButton = instance @@ -865,9 +867,9 @@ describe('LayerPanel', () => { }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - columnId: 'newid', + target: expect.objectContaining({ columnId: 'newid' }), dropType: 'duplicate_compatible', - droppedItem: draggingOperation, + source: draggingOperation, }) ); }); @@ -907,7 +909,7 @@ describe('LayerPanel', () => { humanData: { label: 'Label' }, }; - mockDatasource.onDrop.mockReturnValue({ deleted: 'a' }); + mockDatasource.onDrop.mockReturnValue(true); const updateVisualization = jest.fn(); const { instance } = await mountWithProvider( @@ -925,9 +927,10 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ dropType: 'replace_compatible', - droppedItem: draggingOperation, + source: draggingOperation, }) ); + // testing default onDropForVisualization path expect(mockVis.setDimension).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'c', @@ -945,6 +948,85 @@ describe('LayerPanel', () => { ); expect(updateVisualization).toHaveBeenCalledTimes(1); }); + it('should call onDrop and update visualization when replacing between compatible groups2', async () => { + const mockVis = { + ...mockVisualization, + removeDimension: jest.fn(), + setDimension: jest.fn(() => 'modifiedState'), + onDrop: jest.fn(() => 'modifiedState'), + }; + jest.spyOn(mockVis.onDrop, 'bind').mockImplementation((thisVal, ...args) => mockVis.onDrop); + + mockVis.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'a' }, { columnId: 'b' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [{ columnId: 'c' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup2', + }, + ], + }); + + const draggingOperation = { + layerId: 'first', + columnId: 'a', + groupId: 'a', + id: 'a', + humanData: { label: 'Label' }, + }; + + mockDatasource.onDrop.mockReturnValue(true); + const updateVisualization = jest.fn(); + + const { instance } = await mountWithProvider( + + + + ); + act(() => { + instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible'); + }); + + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dropType: 'replace_compatible', + source: draggingOperation, + }) + ); + + expect(mockVis.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dropType: 'replace_compatible', + prevState: 'state', + source: draggingOperation, + target: expect.objectContaining({ + columnId: 'c', + groupId: 'b', + id: 'c', + layerId: 'first', + }), + }), + mockVis + ); + expect(mockVis.setDimension).not.toHaveBeenCalled(); + expect(mockVis.removeDimension).not.toHaveBeenCalled(); + expect(updateVisualization).toHaveBeenCalledTimes(1); + }); }); describe('add a new dimension', () => { 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 c577bf89d6bd1..0c54ca0df5c71 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 @@ -19,7 +19,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; -import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; +import { + StateSetter, + Visualization, + DragDropOperation, + DropType, + isOperation, +} from '../../../types'; import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; @@ -36,6 +42,7 @@ import { selectResolvedDateRange, selectDatasourceStates, } from '../../../state_management'; +import { onDropForVisualization } from './buttons/drop_targets_utils'; const initialActiveDimensionState = { isNew: false, @@ -109,19 +116,12 @@ export function LayerPanel( const layerDatasourceState = datasourceStates?.[datasourceId]?.state; const layerDatasource = props.datasourceMap[datasourceId]; - const layerDatasourceDropProps = useMemo( - () => ({ - layerId, - state: layerDatasourceState, - setState: (newState: unknown) => { - updateDatasource(datasourceId, newState); - }, - }), - [layerId, layerDatasourceState, datasourceId, updateDatasource] - ); - const layerDatasourceConfigProps = { - ...layerDatasourceDropProps, + state: layerDatasourceState, + setState: (newState: unknown) => { + updateDatasource(datasourceId, newState); + }, + layerId, frame: props.framePublicAPI, dateRange, }; @@ -155,105 +155,70 @@ export function LayerPanel( registerNewRef: registerNewButtonRef, } = useFocusUpdate(allAccessors); - const layerDatasourceOnDrop = layerDatasource?.onDrop; - const onDrop = useMemo(() => { - return ( - droppedItem: DragDropIdentifier, - targetItem: DragDropIdentifier, - dropType?: DropType - ) => { + return (source: DragDropIdentifier, target: DragDropIdentifier, dropType?: DropType) => { if (!dropType) { return; } - const { - columnId, - groupId, - layerId: targetLayerId, - } = targetItem as unknown as DraggedOperation; + if (!isOperation(target)) { + throw new Error('Drop target should be an operation'); + } + if (dropType === 'reorder' || dropType === 'field_replace' || dropType === 'field_add') { - setNextFocusedButtonId(droppedItem.id); + setNextFocusedButtonId(source.id); } else { - setNextFocusedButtonId(columnId); + setNextFocusedButtonId(target.columnId); } + let hasDropSucceeded = true; if (layerDatasource) { - const group = groups.find(({ groupId: gId }) => gId === groupId); - const filterOperations = group?.filterOperations || (() => false); - const dropResult = layerDatasourceOnDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId, - layerId: targetLayerId, - filterOperations, - dimensionGroups: groups, - groupId, - dropType, - }); - if (dropResult) { - let previousColumn = - typeof droppedItem.column === 'string' ? droppedItem.column : undefined; - - // make it inherit only for moving and duplicate - if (!previousColumn) { - // when duplicating check if the previous column is required - if ( - dropType === 'duplicate_compatible' && - typeof droppedItem.columnId === 'string' && - group?.requiresPreviousColumnOnDuplicate - ) { - previousColumn = droppedItem.columnId; - } else { - previousColumn = typeof dropResult === 'object' ? dropResult.deleted : undefined; - } - } - const newVisState = activeVisualization.setDimension({ - columnId, - groupId, - layerId: targetLayerId, - prevState: props.visualizationState, - previousColumn, - frame: framePublicAPI, - }); + hasDropSucceeded = Boolean( + layerDatasource?.onDrop({ + state: layerDatasourceState, + setState: (newState: unknown) => { + updateDatasource(datasourceId, newState); + }, + source, + target: { + ...(target as unknown as DragDropOperation), + filterOperations: + groups.find(({ groupId: gId }) => gId === target.groupId)?.filterOperations || + Boolean, + }, + dimensionGroups: groups, + dropType, + }) + ); + } + if (hasDropSucceeded) { + activeVisualization.onDrop = activeVisualization.onDrop?.bind(activeVisualization); - if (typeof dropResult === 'object') { - // When a column is moved, we delete the reference to the old - updateVisualization( - activeVisualization.removeDimension({ - columnId: dropResult.deleted, - layerId: targetLayerId, - prevState: newVisState, - frame: framePublicAPI, - }) - ); - } else { - updateVisualization(newVisState); - } - } - } else { - if (dropType === 'duplicate_compatible' || dropType === 'reorder') { - const newVisState = activeVisualization.setDimension({ - columnId, - groupId, - layerId: targetLayerId, - prevState: props.visualizationState, - previousColumn: droppedItem.id, - frame: framePublicAPI, - }); - updateVisualization(newVisState); - } + updateVisualization( + (activeVisualization.onDrop || onDropForVisualization)?.( + { + prevState: props.visualizationState, + frame: framePublicAPI, + target, + source, + dropType, + group: groups.find(({ groupId: gId }) => gId === target.groupId), + }, + activeVisualization + ) + ); } }; }, [ layerDatasource, + layerDatasourceState, setNextFocusedButtonId, groups, - layerDatasourceOnDrop, - layerDatasourceDropProps, activeVisualization, props.visualizationState, framePublicAPI, updateVisualization, + datasourceId, + updateDatasource, ]); const isDimensionPanelOpen = Boolean(activeId); @@ -462,15 +427,15 @@ export function LayerPanel( return ( setHideTooltip(true)} @@ -562,12 +527,12 @@ export function LayerPanel( {group.supportsMoreColumns ? ( { props.onEmptyDimensionAdd(id, group); setActiveDimension({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 66a30b0a405e8..172e0702f56e8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -29,7 +29,6 @@ export interface LayerPanelProps { } export interface LayerDatasourceDropProps { - layerId: string; state: unknown; setState: (newState: unknown) => void; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts deleted file mode 100644 index 66714f494bf53..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ /dev/null @@ -1,2332 +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 { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { IndexPatternDimensionEditorProps } from '../dimension_panel'; -import { onDrop } from './on_drop_handler'; -import { getDropProps } from './get_drop_props'; -import { - IUiSettingsClient, - SavedObjectsClientContract, - HttpSetup, - CoreSetup, -} from '@kbn/core/public'; -import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; -import { IndexPatternLayer, IndexPatternPrivateState } from '../../types'; -import { documentField } from '../../document_field'; -import { OperationMetadata, DropType } from '../../../types'; -import { - DateHistogramIndexPatternColumn, - GenericIndexPatternColumn, - MedianIndexPatternColumn, - TermsIndexPatternColumn, -} from '../../operations'; -import { getFieldByNameFactory } from '../../pure_helpers'; -import { generateId } from '../../../id_generator'; -import { layerTypes } from '../../../../common'; - -jest.mock('../../../id_generator'); - -const 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, - }, - { - name: 'src', - displayName: 'src', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - documentField, -]; - -const expectedIndexPatterns = { - foo: { - id: 'foo', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasExistence: true, - hasRestrictions: false, - fields, - getFieldByName: getFieldByNameFactory(fields), - }, -}; - -const dimensionGroups = [ - { - accessors: [], - groupId: 'a', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: (op: OperationMetadata) => op.isBucketed, - }, - { - accessors: [{ columnId: 'col1' }, { columnId: 'col2' }, { columnId: 'col3' }], - groupId: 'b', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: (op: OperationMetadata) => op.isBucketed, - }, - { - accessors: [{ columnId: 'col4' }], - groupId: 'c', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }, -]; - -const oneColumnLayer: IndexPatternLayer = { - indexPatternId: 'foo', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Date histogram of timestamp', - customLabel: true, - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - } as DateHistogramIndexPatternColumn, - }, - incompleteColumns: {}, -}; - -const multipleColumnsLayer: IndexPatternLayer = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: oneColumnLayer.columns.col1, - col2: { - label: 'Top 10 values of src', - dataType: 'string', - isBucketed: true, - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'src', - } as TermsIndexPatternColumn, - col3: { - label: 'Top 10 values of dest', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'dest', - } as TermsIndexPatternColumn, - col4: { - label: 'Median of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'median', - sourceField: 'bytes', - }, - }, -}; - -const draggingField = { - field: { type: 'number', name: 'bytes', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, -}; - -const draggingCol1 = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Column 1' }, -}; - -const draggingCol2 = { - columnId: 'col2', - groupId: 'b', - layerId: 'first', - id: 'col2', - humanData: { label: 'Column 2' }, - filterOperations: (op: OperationMetadata) => op.isBucketed, -}; - -const draggingCol3 = { - columnId: 'col3', - groupId: 'b', - layerId: 'first', - id: 'col3', - humanData: { - label: '', - }, -}; - -const draggingCol4 = { - columnId: 'col4', - groupId: 'c', - layerId: 'first', - id: 'col4', - humanData: { - label: '', - }, - filterOperations: (op: OperationMetadata) => op.isBucketed === false, -}; - -/** - * 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 - * - * - getDropProps: Returns drop types that are possible for the current dragging field or other dimension - * - onDrop: Correct application of drop logic - */ -describe('IndexPatternDimensionEditorPanel', () => { - let state: IndexPatternPrivateState; - let setState: jest.Mock; - let defaultProps: IndexPatternDimensionEditorProps; - - function getStateWithMultiFieldColumn() { - return { - ...state, - layers: { - ...state.layers, - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Top values of dest', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'dest', - } as TermsIndexPatternColumn, - }, - }, - }, - }; - } - - beforeEach(() => { - state = { - indexPatternRefs: [], - indexPatterns: expectedIndexPatterns, - currentIndexPatternId: 'foo', - isFirstExistenceFetch: false, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - bytes: true, - memory: true, - source: true, - }, - }, - layers: { first: { ...oneColumnLayer } }, - }; - - setState = jest.fn(); - - defaultProps = { - state, - setState, - dateRange: { fromDate: 'now-1d', toDate: 'now' }, - columnId: 'col1', - layerId: 'first', - uniqueLabel: 'stuff', - groupId: 'group1', - 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, - unifiedSearch: {} as UnifiedSearchPublicPluginStart, - dataViews: {} as DataViewsPublicPluginStart, - core: {} as CoreSetup, - dimensionGroups: [], - isFullscreen: false, - toggleFullscreen: () => {}, - supportStaticValue: false, - layerType: layerTypes.DATA, - }; - - jest.clearAllMocks(); - }); - - const groupId = 'a'; - - describe('getDropProps', () => { - it('returns undefined if no drag is happening', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { name: 'bar', id: 'bar', humanData: { label: 'Label' } }, - }) - ).toBe(undefined); - }); - - it('returns undefined if the dragged item has no field', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - name: 'bar', - id: 'bar', - humanData: { label: 'Label' }, - }, - }) - ).toBe(undefined); - }); - - describe('dragging a field', () => { - it('returns undefined if field is not supported by filterOperations', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: draggingField, - filterOperations: () => false, - }) - ).toBe(undefined); - }); - - it('returns field_replace if the field is supported by filterOperations and the dropTarget is an existing column', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toEqual({ dropTypes: ['field_replace'], nextLabel: 'Intervals' }); - }); - - it('returns field_add if the field is supported by filterOperations and the dropTarget is an empty column', () => { - expect( - getDropProps({ - ...defaultProps, - columnId: 'newId', - groupId, - dragging: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toEqual({ dropTypes: ['field_add'], nextLabel: 'Intervals' }); - }); - - it('returns undefined if the field belongs to another index pattern', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - id: 'bar', - humanData: { label: 'Label' }, - }, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toBe(undefined); - }); - - it('returns undefined if the dragged field is already in use by this operation', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - field: { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, - }, - }) - ).toBe(undefined); - }); - - it('returns also field_combine if the field is supported by filterOperations and the dropTarget is an existing column that supports multiple fields', () => { - // replace the state with a top values column to enable the multi fields behaviour - state = getStateWithMultiFieldColumn(); - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType !== 'date', - }) - ).toEqual({ dropTypes: ['field_replace', 'field_combine'] }); - }); - }); - - describe('dragging a column', () => { - it('returns undefined if the dragged column from different group uses the same field as the dropTarget', () => { - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Date histogram of timestamp (1)', - customLabel: true, - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - } as DateHistogramIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual(undefined); - }); - - it('returns undefined if the dragged column from different group uses the same fields as the dropTarget', () => { - state = getStateWithMultiFieldColumn(); - const sourceMultiFieldColumn = { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['dest'], - }, - } as TermsIndexPatternColumn; - // invert the fields - const targetMultiFieldColumn = { - ...state.layers.first.columns.col1, - sourceField: 'dest', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['bytes'], - }, - } as TermsIndexPatternColumn; - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: sourceMultiFieldColumn, - col2: targetMultiFieldColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual(undefined); - }); - - it('returns duplicate and replace if the dragged column from different group uses the same field as the dropTarget, but this last one is multifield, and can be swappable', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['dest'], - }, - } as TermsIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], - }); - }); - - it('returns swap, duplicate and replace if the dragged column from different group uses the same field as the dropTarget, but this last one is multifield', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['dest'], - }, - } as TermsIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - // make it swappable - dimensionGroups: [ - { - accessors: [{ columnId: 'col1' }], - filterOperations: jest.fn(() => true), - groupId, - groupLabel: '', - supportsMoreColumns: false, - }, - ], - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], - }); - }); - - it('returns reorder if drop target and droppedItem columns are from the same group and both are existing', () => { - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { ...draggingCol1, groupId }, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }) - ).toEqual({ - dropTypes: ['reorder'], - }); - }); - - it('returns duplicate_compatible if drop target and droppedItem columns are from the same group and drop target id is a new column', () => { - expect( - getDropProps({ - ...defaultProps, - columnId: 'newId', - groupId, - dragging: { - ...draggingCol1, - groupId, - }, - }) - ).toEqual({ dropTypes: ['duplicate_compatible'] }); - }); - - it('returns compatible drop types if the dragged column is compatible', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ dropTypes: ['move_compatible', 'duplicate_compatible'] }); - }); - - it('returns incompatible drop target types if dropping column to existing incompatible column', () => { - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }) - ).toEqual({ - dropTypes: [ - 'replace_incompatible', - 'replace_duplicate_incompatible', - 'swap_incompatible', - ], - nextLabel: 'Minimum', - }); - }); - - it('does not return swap_incompatible if current dropTarget column cannot be swapped to the group of dragging column', () => { - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - filterOperations: (op: OperationMetadata) => op.isBucketed === true, - }, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }) - ).toEqual({ - dropTypes: ['replace_incompatible', 'replace_duplicate_incompatible'], - nextLabel: 'Minimum', - }); - }); - - it('returns combine_compatible drop type if the dragged column is compatible and the target one support multiple fields', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'combine_compatible'], - }); - }); - - it('returns no combine_compatible drop type if the target column uses rarity ordering', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - orderBy: { type: 'rare' }, - }, - } as TermsIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], - }); - }); - - it('returns no combine drop type if the dragged column is compatible, the target one supports multiple fields but there are too many fields', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'source', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['memory', 'bytes', 'geo.src'], // too many fields here - }, - } as TermsIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], - }); - }); - - it('returns combine_incompatible drop target types if dropping column to existing incompatible column which supports multiple fields', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - // drag the sum over the top values - dragging: { - ...draggingCol2, - groupId: 'c', - filterOperation: undefined, - }, - columnId: 'col1', - filterOperations: (op: OperationMetadata) => op.isBucketed, - }) - ).toEqual({ - dropTypes: [ - 'replace_incompatible', - 'replace_duplicate_incompatible', - 'swap_incompatible', - 'combine_incompatible', - ], - nextLabel: 'Top values', - }); - }); - }); - }); - - describe('onDrop', () => { - describe('dropping a field', () => { - it('updates a column when a field is dropped', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }), - }), - }, - }); - }); - it('selects the specific operation that was valid on drop', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed, - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col1', 'col2'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }, - }, - }, - }); - }); - it('keeps the operation when dropping a different compatible field', () => { - onDrop({ - ...defaultProps, - droppedItem: { - field: { name: 'memory', type: 'number', aggregatable: true }, - indexPatternId: 'foo', - id: '1', - }, - state: { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }, - }, - }, - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - operationType: 'sum', - dataType: 'number', - sourceField: 'memory', - }), - }), - }), - }, - }); - }); - it('appends the dropped column when a field is dropped', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - dropType: 'field_replace', - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col1', 'col2'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }, - }, - }, - }); - }); - it('dimensionGroups are defined - appends the dropped column in the right place when a field is dropped', () => { - const testState = { ...state }; - testState.layers.first = { ...multipleColumnsLayer }; - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging field into newCol in group a - - onDrop({ - ...defaultProps, - droppedItem: draggingField, - columnId: 'newCol', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: 'a', - dimensionGroups, - dropType: 'field_add', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], - columns: { - newCol: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('appends the new field to the column that supports multiple fields when a field is dropped', () => { - state = getStateWithMultiFieldColumn(); - onDrop({ - ...defaultProps, - state, - droppedItem: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - dropType: 'field_combine', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'string', - sourceField: 'dest', - params: expect.objectContaining({ secondaryFields: ['bytes'] }), - }), - }), - }), - }, - }); - }); - }); - - describe('dropping a dimension', () => { - const dragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - - it('sets correct order in group for metric and bucket columns when duplicating a column in group', () => { - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - } as DateHistogramIndexPatternColumn, - col2: { - label: 'Top values of bar', - dataType: 'number', - isBucketed: true, - operationType: 'terms', - sourceField: 'bar', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'asc', - size: 5, - }, - } as TermsIndexPatternColumn, - col3: { - operationType: 'average', - sourceField: 'memory', - label: 'average of memory', - dataType: 'number', - isBucketed: false, - }, - }, - }, - }, - }; - - const referenceDragging = { - columnId: 'col3', - groupId: 'a', - layerId: 'first', - id: 'col3', - humanData: { label: 'Label' }, - }; - - onDrop({ - ...defaultProps, - droppedItem: referenceDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'newCol', - }); - // metric is appended - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'newCol'], - columns: { - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - newCol: testState.layers.first.columns.col3, - }, - }, - }, - }); - - const bucketDragging = { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - humanData: { label: 'Label' }, - }; - - onDrop({ - ...defaultProps, - droppedItem: bucketDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'newCol', - }); - - // bucket is placed after the last existing bucket - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'newCol', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - newCol: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - }, - }, - }); - }); - - it('when duplicating fullReference column, the referenced columns get duplicated too', () => { - (generateId as jest.Mock).mockReturnValue(`ref1Copy`); - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1', 'ref1'], - columns: { - col1: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['ref1'], - }, - ref1: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - }, - }, - }, - }; - const referenceDragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - onDrop({ - ...defaultProps, - droppedItem: referenceDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'col1Copy', - }); - - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'ref1', 'ref1Copy', 'col1Copy'], - columns: { - ref1: testState.layers.first.columns.ref1, - col1: testState.layers.first.columns.col1, - ref1Copy: { ...testState.layers.first.columns.ref1 }, - col1Copy: { - ...testState.layers.first.columns.col1, - references: ['ref1Copy'], - }, - }, - }, - }, - }); - }); - - it('when duplicating fullReference column, the multiple referenced columns get duplicated too', () => { - (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`); - (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`); - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1', 'ref1'], - columns: { - col1: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['ref1', 'ref2'], - }, - ref1: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - ref2: { - label: 'Unique count of bytes', - dataType: 'number', - isBucketed: false, - sourceField: 'bytes', - operationType: 'unique_count', - }, - }, - }, - }, - }; - const metricDragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - onDrop({ - ...defaultProps, - droppedItem: metricDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'col1Copy', - }); - - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'ref1', 'ref2', 'ref1Copy', 'col1Copy', 'ref2Copy'], - columns: { - ref1: testState.layers.first.columns.ref1, - ref2: testState.layers.first.columns.ref2, - col1: testState.layers.first.columns.col1, - ref2Copy: { ...testState.layers.first.columns.ref2 }, - ref1Copy: { ...testState.layers.first.columns.ref1 }, - col1Copy: { - ...testState.layers.first.columns.col1, - references: ['ref1Copy', 'ref2Copy'], - }, - }, - }, - }, - }); - }); - - it('when duplicating fullReference column, the referenced columns get duplicated recursively', () => { - (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`); - (generateId as jest.Mock).mockReturnValueOnce(`innerRef1Copy`); - (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`); - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['innerRef1', 'ref2', 'ref1', 'col1'], - columns: { - col1: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['ref1', 'ref2'], - }, - ref1: { - label: 'Reference that has a reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['innerRef1'], - }, - innerRef1: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - ref2: { - label: 'Unique count of bytes', - dataType: 'number', - isBucketed: false, - sourceField: 'bytes', - operationType: 'unique_count', - }, - }, - }, - }, - }; - const refDragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - onDrop({ - ...defaultProps, - droppedItem: refDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'col1Copy', - }); - - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: [ - 'innerRef1', - 'ref2', - 'ref1', - 'col1', - 'innerRef1Copy', - 'ref1Copy', - 'col1Copy', - 'ref2Copy', - ], - columns: { - innerRef1: testState.layers.first.columns.innerRef1, - ref1: testState.layers.first.columns.ref1, - ref2: testState.layers.first.columns.ref2, - col1: testState.layers.first.columns.col1, - - innerRef1Copy: { ...testState.layers.first.columns.innerRef1 }, - ref2Copy: { ...testState.layers.first.columns.ref2 }, - ref1Copy: { - ...testState.layers.first.columns.ref1, - references: ['innerRef1Copy'], - }, - col1Copy: { - ...testState.layers.first.columns.col1, - references: ['ref1Copy', 'ref2Copy'], - }, - }, - }, - }, - }); - }); - - it('when duplicating fullReference column onto exisitng column, the state will not get modified', () => { - (generateId as jest.Mock).mockReturnValue(`ref1Copy`); - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col2', 'ref1', 'col1'], - columns: { - col1: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['ref1'], - }, - ref1: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - col2: { - label: 'Minimum', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'min', - sourceField: 'bytes', - customLabel: true, - }, - }, - }, - }, - }; - const referenceDragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - onDrop({ - ...defaultProps, - droppedItem: referenceDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'col2', - }); - - expect(setState).toHaveBeenCalledWith(testState); - }); - - it('sets correct order in group when reordering a column in group', () => { - const testState = { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - } as GenericIndexPatternColumn, - col2: { - label: 'Top values of bar', - dataType: 'number', - isBucketed: true, - } as GenericIndexPatternColumn, - col3: { - label: 'Top values of memory', - dataType: 'number', - isBucketed: true, - } as GenericIndexPatternColumn, - }, - }, - }, - }; - - const defaultReorderDropParams = { - ...defaultProps, - dragging, - droppedItem: draggingCol1, - state: testState, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - dropType: 'reorder' as DropType, - }; - - const stateWithColumnOrder = (columnOrder: string[]) => { - return { - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder, - columns: { - ...testState.layers.first.columns, - }, - }, - }, - }; - }; - - // first element to last - onDrop({ - ...defaultReorderDropParams, - columnId: 'col3', - }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col3', 'col1'])); - - // last element to first - onDrop({ - ...defaultReorderDropParams, - columnId: 'col1', - droppedItem: { - columnId: 'col3', - groupId: 'a', - layerId: 'first', - id: 'col3', - }, - }); - expect(setState).toBeCalledTimes(2); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col3', 'col1', 'col2'])); - - // middle column to first - onDrop({ - ...defaultReorderDropParams, - columnId: 'col1', - droppedItem: { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - }, - }); - expect(setState).toBeCalledTimes(3); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col1', 'col3'])); - - // middle column to last - onDrop({ - ...defaultReorderDropParams, - columnId: 'col3', - droppedItem: { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - }, - }); - expect(setState).toBeCalledTimes(4); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col1', 'col3', 'col2'])); - }); - - it('updates the column id when moving an operation to an empty dimension', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingCol1, - columnId: 'col2', - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col2'], - columns: { - col2: state.layers.first.columns.col1, - }, - }, - }, - }); - }); - - it('replaces an operation when moving to a populated dimension', () => { - const testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - - col2: { - label: 'Top 10 values of src', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col3' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'src', - } as TermsIndexPatternColumn, - col3: { - label: 'Count', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', - sourceField: '___records___', - customLabel: true, - }, - }, - }; - - onDrop({ - ...defaultProps, - droppedItem: draggingCol2, - state: testState, - dropType: 'replace_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col3'], - columns: { - col1: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - }, - }, - }); - }); - - it('when combine compatible columns should append dropped column fields into the target one', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first.columns = { - ...state.layers.first.columns, - col2: { - isBucketed: true, - label: 'Top values of source', - operationType: 'terms', - sourceField: 'bytes', - dataType: 'number', - params: { - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'desc', - size: 10, - }, - } as TermsIndexPatternColumn, - }; - onDrop({ - ...defaultProps, - state, - droppedItem: { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - humanData: { label: 'Label' }, - }, - filterOperations: (op: OperationMetadata) => op.isBucketed, - dropType: 'combine_compatible', - columnId: 'col1', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'string', - sourceField: 'dest', - params: expect.objectContaining({ secondaryFields: ['bytes'] }), - }), - }), - }), - }, - }); - }); - - describe('dimension group aware ordering and copying', () => { - let testState: IndexPatternPrivateState; - beforeEach(() => { - testState = { ...state }; - testState.layers.first = { ...multipleColumnsLayer }; - }); - - it('respects groups on moving operations between compatible groups', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging col2 into newCol in group a - onDrop({ - ...defaultProps, - columnId: 'newCol', - droppedItem: draggingCol2, - state: testState, - groupId: 'a', - dimensionGroups, - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col3', 'col4'], - columns: { - newCol: testState.layers.first.columns.col2, - col1: testState.layers.first.columns.col1, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('respects groups on duplicating operations between compatible groups', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging col2 into newCol in group a - onDrop({ - ...defaultProps, - columnId: 'newCol', - droppedItem: draggingCol2, - state: testState, - groupId: 'a', - dimensionGroups, - dropType: 'duplicate_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], - columns: { - newCol: testState.layers.first.columns.col2, - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('respects groups on moving operations between compatible groups with overwrite', () => { - // config: - // a: col1, - // b: col2, col3 - // c: col4 - // dragging col3 onto col1 in group a - onDrop({ - ...defaultProps, - columnId: 'col1', - droppedItem: draggingCol3, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col4'], - columns: { - col1: testState.layers.first.columns.col3, - col2: testState.layers.first.columns.col2, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('respects groups on moving operations if some columns are not listed in groups', () => { - // config: - // a: col1, - // b: col2, col3 - // c: col4 - // col5, col6 not in visualization groups - // dragging col3 onto col1 in group a - onDrop({ - ...defaultProps, - columnId: 'col1', - droppedItem: draggingCol3, - state: { - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'], - columns: { - ...testState.layers.first.columns, - col5: { - dataType: 'number', - operationType: 'count', - label: '', - isBucketed: false, - sourceField: '___records___', - customLabel: true, - }, - col6: { - dataType: 'number', - operationType: 'count', - label: '', - isBucketed: false, - sourceField: '___records___', - customLabel: true, - }, - }, - }, - }, - }, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col4', 'col5', 'col6'], - columns: { - col1: testState.layers.first.columns.col3, - col2: testState.layers.first.columns.col2, - col4: testState.layers.first.columns.col4, - col5: expect.objectContaining({ - dataType: 'number', - operationType: 'count', - label: '', - isBucketed: false, - sourceField: '___records___', - }), - col6: expect.objectContaining({ - dataType: 'number', - operationType: 'count', - label: '', - isBucketed: false, - sourceField: '___records___', - }), - }, - }, - }, - }); - }); - - it('respects groups on duplicating operations between compatible groups with overwrite', () => { - // config: - // a: col1, - // b: col2, col3 - // c: col4 - // dragging col3 onto col1 in group a - - onDrop({ - ...defaultProps, - columnId: 'col1', - droppedItem: draggingCol3, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - dropType: 'duplicate_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col3, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('moves newly created dimension to the bottom of the current group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col1 into newCol in group b - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'move_compatible', - droppedItem: draggingCol1, - state: testState, - groupId: 'b', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col2', 'col3', 'newCol', 'col4'], - columns: { - newCol: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('copies column to the bottom of the current group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // copying col1 within group a - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'duplicate_compatible', - droppedItem: draggingCol1, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col1, - newCol: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('appends the dropped column in the right place respecting custom nestingOrder', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging field into newCol in group a - - onDrop({ - ...defaultProps, - droppedItem: draggingField, - columnId: 'newCol', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: 'a', - dimensionGroups: [ - // a and b are ordered in reverse visually, but nesting order keeps them in place for column order - { ...dimensionGroups[1], nestingOrder: 1 }, - { ...dimensionGroups[0], nestingOrder: 0 }, - { ...dimensionGroups[2] }, - ], - dropType: 'field_add', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], - columns: { - newCol: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('moves incompatible column to the bottom of the target group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into newCol in group a - - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'move_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - newCol: expect.objectContaining({ - sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) - .sourceField, - }), - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('copies incompatible column to the bottom of the target group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into newCol in group a - - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'duplicate_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col1, - newCol: expect.objectContaining({ - sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) - .sourceField, - }), - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('moves incompatible column with overwrite keeping order of target column', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into col2 in group b - - onDrop({ - ...defaultProps, - columnId: 'col2', - dropType: 'move_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'b', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - col2: { - isBucketed: true, - label: 'Top 10 values of bytes', - operationType: 'terms', - sourceField: 'bytes', - dataType: 'number', - params: { - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'desc', - size: 10, - parentFormat: { id: 'terms' }, - }, - }, - col3: testState.layers.first.columns.col3, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('when swapping compatibly, columns carry order', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into col1 - - onDrop({ - ...defaultProps, - columnId: 'col1', - dropType: 'swap_compatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col4, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col1, - }, - }, - }, - }); - }); - - it('when swapping incompatibly, newly created columns take order from the columns they replace', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into col2 - - onDrop({ - ...defaultProps, - columnId: 'col2', - dropType: 'swap_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'b', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col1, - col2: { - isBucketed: true, - label: 'Top 10 values of bytes', - operationType: 'terms', - sourceField: 'bytes', - dataType: 'number', - params: { - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'desc', - parentFormat: { id: 'terms' }, - size: 10, - }, - }, - col3: testState.layers.first.columns.col3, - col4: { - isBucketed: false, - label: 'Unique count of src', - filter: undefined, - operationType: 'unique_count', - sourceField: 'src', - timeShift: undefined, - dataType: 'number', - params: { - emptyAsNull: true, - }, - scale: 'ratio', - }, - }, - incompleteColumns: {}, - }, - }, - }); - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts new file mode 100644 index 0000000000000..f9afc9a00c98f --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts @@ -0,0 +1,767 @@ +/* + * 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 { DragDropOperation, OperationMetadata } from '../../../types'; +import { TermsIndexPatternColumn } from '../../operations'; +import { getDropProps } from './get_drop_props'; +import { + mockDataViews, + mockedLayers, + mockedDraggedField, + mockedDndOperations, + mockedColumns, +} from './mocks'; +import { generateId } from '../../../id_generator'; + +const getDefaultProps = () => ({ + state: { + indexPatternRefs: [], + indexPatterns: mockDataViews(), + currentIndexPatternId: 'first', + isFirstExistenceFetch: false, + existingFields: { + first: { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + layers: { first: mockedLayers.doubleColumnLayer(), second: mockedLayers.emptyLayer() }, + }, + target: mockedDndOperations.notFiltering, + source: mockedDndOperations.bucket, +}); + +describe('IndexPatternDimensionEditorPanel#getDropProps', () => { + describe('not dragging', () => { + it('returns undefined if no drag is happening', () => { + expect(getDropProps({ ...getDefaultProps(), source: undefined })).toBe(undefined); + }); + + it('returns undefined if the dragged item has no field', () => { + expect( + getDropProps({ + ...getDefaultProps(), + source: { name: 'bar', id: 'bar', humanData: { label: 'Label' } }, + }) + ).toBe(undefined); + }); + }); + + describe('dragging a field', () => { + it('returns undefined if field is not supported by filterOperations', () => { + expect( + getDropProps({ + ...getDefaultProps(), + source: mockedDraggedField, + target: mockedDndOperations.staticValue, + }) + ).toBe(undefined); + }); + + it('returns field_replace if the field is supported by filterOperations and the dropTarget is an existing column', () => { + expect( + getDropProps({ + ...getDefaultProps(), + target: mockedDndOperations.numericalOnly, + source: mockedDraggedField, + }) + ).toEqual({ dropTypes: ['field_replace'], nextLabel: 'Intervals' }); + }); + + it('returns field_add if the field is supported by filterOperations and the dropTarget is an empty column', () => { + expect( + getDropProps({ + ...getDefaultProps(), + target: { + ...mockedDndOperations.numericalOnly, + columnId: 'newId', + }, + source: mockedDraggedField, + }) + ).toEqual({ dropTypes: ['field_add'], nextLabel: 'Intervals' }); + }); + + it('returns undefined if the field belongs to another data view', () => { + expect( + getDropProps({ + ...getDefaultProps(), + source: { + ...mockedDraggedField, + indexPatternId: 'first2', + }, + }) + ).toBe(undefined); + }); + + it('returns undefined if the dragged field is already in use by this operation', () => { + expect( + getDropProps({ + ...getDefaultProps(), + source: { + ...mockedDraggedField, + field: { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + }, + }) + ).toBe(undefined); + }); + + it('returns also field_combine if the field is supported by filterOperations and the dropTarget is an existing column that supports multiple fields', () => { + // replace the state with a top values column to enable the multi fields behaviour + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + source: mockedDraggedField, + target: { + ...props.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType !== 'date', + }, + }) + ).toEqual({ dropTypes: ['field_replace', 'field_combine'] }); + }); + }); + + describe('dragging a column', () => { + it('allows replacing and replace-duplicating when two columns from compatible groups use the same field', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = mockedColumns.dateHistogramCopy; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ dropTypes: ['replace_compatible', 'replace_duplicate_compatible'] }); + }); + + it('returns correct dropTypes if the dragged column from different group uses the same fields as the dropTarget', () => { + const props = getDefaultProps(); + const sourceMultiFieldColumn = { + ...props.state.layers.first.columns.col1, + sourceField: 'bytes', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['dest'], + }, + } as TermsIndexPatternColumn; + // invert the fields + const targetMultiFieldColumn = { + ...props.state.layers.first.columns.col1, + sourceField: 'dest', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['bytes'], + }, + } as TermsIndexPatternColumn; + props.state.layers.first.columns = { + col1: sourceMultiFieldColumn, + col2: targetMultiFieldColumn, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ dropTypes: ['replace_compatible', 'replace_duplicate_compatible'] }); + }); + + it('returns duplicate and replace if the dragged column from different group uses the same field as the dropTarget, but this last one is multifield, and can be swappable', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = { + ...props.state.layers.first.columns.col1, + sourceField: 'bytes', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['dest'], + }, + } as TermsIndexPatternColumn; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], + }); + }); + + it('returns swap, duplicate and replace if the dragged column from different group uses the same field as the dropTarget, but this last one is multifield', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = { + ...props.state.layers.first.columns.col1, + sourceField: 'bytes', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['dest'], + }, + } as TermsIndexPatternColumn; + + expect( + getDropProps({ + ...props, + ...props, + // make it swappable + target: { + ...props.target, + filterOperations: (op: OperationMetadata) => op.isBucketed, + groupId: 'a', + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + filterOperations: (op: OperationMetadata) => op.isBucketed, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], + }); + }); + + it('returns reorder if drop target and source columns are from the same group and both are existing', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = mockedColumns.sum; + + expect( + getDropProps({ + ...props, + source: { ...mockedDndOperations.metric, groupId: 'a' }, + target: { + ...props.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, + }) + ).toEqual({ + dropTypes: ['reorder'], + }); + }); + + it('returns duplicate_compatible if drop target and source columns are from the same group and drop target id is a new column', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + columnId: 'newId', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'a', + }, + }) + ).toEqual({ dropTypes: ['duplicate_compatible'] }); + }); + + it('returns compatible drop types if the dragged column is compatible', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + columnId: 'col3', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ dropTypes: ['move_compatible', 'duplicate_compatible'] }); + }); + + it('returns incompatible drop target types if dropping column to existing incompatible column', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.dateHistogram, + col2: mockedColumns.sum, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_incompatible', 'replace_duplicate_incompatible', 'swap_incompatible'], + nextLabel: 'Minimum', + }); + }); + + it('does not return swap_incompatible if current dropTarget column cannot be swapped to the group of dragging column', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.dateHistogram, + col2: mockedColumns.count, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, + source: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => op.isBucketed === true, + }, + }) + ).toEqual({ + dropTypes: ['replace_incompatible', 'replace_duplicate_incompatible'], + nextLabel: 'Minimum', + }); + }); + + it('returns combine_compatible drop type if the dragged column is compatible and the target one support multiple fields', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.terms, + col2: { + ...mockedColumns.terms, + sourceField: 'bytes', + }, + }; + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'combine_compatible'], + }); + }); + + it('returns no combine_compatible drop type if the target column uses rarity ordering', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.terms, + col2: { + ...mockedColumns.terms, + sourceField: 'bytes', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + orderBy: { type: 'rare' }, + }, + } as TermsIndexPatternColumn, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], + }); + }); + + it('returns no combine drop type if the dragged column is compatible, the target one supports multiple fields but there are too many fields', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = { + ...props.state.layers.first.columns.col1, + sourceField: 'source', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['memory', 'bytes', 'geo.src'], // too many fields here + }, + } as TermsIndexPatternColumn; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], + }); + }); + + it('returns combine_incompatible drop target types if dropping column to existing incompatible column which supports multiple fields', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.terms, + col2: mockedColumns.sum, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + // drag the sum over the top values + source: { + ...mockedDndOperations.bucket, + groupId: 'c', + filterOperation: undefined, + }, + }) + ).toEqual({ + dropTypes: [ + 'replace_incompatible', + 'replace_duplicate_incompatible', + 'swap_incompatible', + 'combine_incompatible', + ], + nextLabel: 'Top values', + }); + }); + }); + + describe('getDropProps between layers', () => { + it('allows dropping to the same group', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + source: { + ...mockedDndOperations.metric, + columnId: 'col1', + layerId: 'first', + groupId: 'c', + }, + target: { + ...props.target, + columnId: 'newId', + groupId: 'c', + layerId: 'second', + }, + }) + ).toEqual({ + dropTypes: ['move_compatible', 'duplicate_compatible'], + }); + }); + it('allows dropping to compatible groups', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + source: { + ...mockedDndOperations.metric, + columnId: 'col1', + layerId: 'first', + groupId: 'a', + }, + target: { + ...props.target, + columnId: 'newId', + groupId: 'c', + layerId: 'second', + }, + }) + ).toEqual({ + dropTypes: ['move_compatible', 'duplicate_compatible'], + }); + }); + it('allows incompatible drop', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + source: { + ...mockedDndOperations.metric, + columnId: 'col1', + layerId: 'first', + groupId: 'c', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + target: { + ...props.target, + columnId: 'newId', + groupId: 'c', + layerId: 'second', + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + })?.dropTypes + ).toEqual(['move_incompatible', 'duplicate_incompatible']); + }); + it('allows dropping references', () => { + const props = getDefaultProps(); + const referenceDragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + + (generateId as jest.Mock).mockReturnValue(`ref1Copy`); + props.state = { + ...props.state, + layers: { + ...props.state.layers, + first: { + indexPatternId: 'first', + columnOrder: ['col1', 'ref1'], + columns: { + col1: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['ref1'], + }, + ref1: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + }, + }, + }, + }; + + expect( + getDropProps({ + ...props, + source: referenceDragging, + target: { + ...props.target, + columnId: 'newColumnId', + groupId: 'c', + layerId: 'second', + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + })?.dropTypes + ).toEqual(['move_compatible', 'duplicate_compatible']); + }); + it('doesnt allow dropping for different index patterns', () => { + const props = getDefaultProps(); + props.state.layers.second.indexPatternId = 'different index'; + expect( + getDropProps({ + ...props, + source: { + ...mockedDndOperations.metric, + columnId: 'col1', + layerId: 'first', + groupId: 'c', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + target: { + ...props.target, + columnId: 'newId', + groupId: 'c', + layerId: 'second', + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + })?.dropTypes + ).toEqual(undefined); + }); + + it('does not allow static value to be moved when not allowed', () => { + const props = getDefaultProps(); + props.state.layers = { + first: { + indexPatternId: 'first', + columns: { + col1: mockedColumns.dateHistogram, + colMetric: mockedColumns.count, + }, + columnOrder: ['col1', 'colMetric'], + incompleteColumns: {}, + }, + second: { + indexPatternId: 'first', + columns: { + staticValue: mockedColumns.staticValue, + }, + columnOrder: ['staticValue'], + incompleteColumns: {}, + }, + }; + expect( + getDropProps({ + ...props, + source: { + columnId: 'staticValue', + groupId: 'yReferenceLineLeft', + layerId: 'second', + id: 'staticValue', + humanData: { label: 'Label' }, + }, + target: { + layerId: 'first', + columnId: 'col1', + groupId: 'x', + } as DragDropOperation, + })?.dropTypes + ).toEqual(undefined); + }); + it('allow multiple drop types from terms to terms', () => { + const props = getDefaultProps(); + props.state.layers = { + first: { + indexPatternId: 'first', + columns: { + terms: mockedColumns.terms, + metric: mockedColumns.count, + }, + columnOrder: ['terms', 'metric'], + incompleteColumns: {}, + }, + second: { + indexPatternId: 'first', + columns: { + terms2: mockedColumns.terms2, + metric2: mockedColumns.count, + }, + columnOrder: ['terms2', 'metric2'], + incompleteColumns: {}, + }, + }; + expect( + getDropProps({ + ...props, + source: { + columnId: 'terms', + groupId: 'x', + layerId: 'first', + id: 'terms', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + target: { + columnId: 'terms2', + groupId: 'x', + layerId: 'second', + filterOperations: (op: OperationMetadata) => op.isBucketed, + } as DragDropOperation, + })?.dropTypes + ).toEqual([ + 'replace_compatible', + 'replace_duplicate_compatible', + 'swap_compatible', + 'combine_compatible', + ]); + }); + it('allow multiple drop types from metric on field to terms', () => { + const props = getDefaultProps(); + props.state.layers = { + first: { + indexPatternId: 'first', + columns: { + sum: mockedColumns.sum, + metric: mockedColumns.count, + }, + columnOrder: ['sum', 'metric'], + incompleteColumns: {}, + }, + second: { + indexPatternId: 'first', + columns: { + terms2: mockedColumns.terms2, + metric2: mockedColumns.count, + }, + columnOrder: ['terms2', 'metric2'], + incompleteColumns: {}, + }, + }; + expect( + getDropProps({ + ...props, + source: { + columnId: 'sum', + groupId: 'x', + layerId: 'first', + id: 'sum', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + target: { + columnId: 'terms2', + groupId: 'x', + layerId: 'second', + filterOperations: (op: OperationMetadata) => op.isBucketed, + } as DragDropOperation, + })?.dropTypes + ).toEqual([ + 'replace_incompatible', + 'replace_duplicate_incompatible', + 'swap_incompatible', + 'combine_incompatible', + ]); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts index 3318b8c30909e..093746134caf7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts @@ -5,13 +5,7 @@ * 2.0. */ -import { - DatasourceDimensionDropProps, - isDraggedOperation, - DraggedOperation, - DropType, - VisualizationDimensionGroupConfig, -} from '../../../types'; +import { isOperation, DropType, DragDropOperation } from '../../../types'; import { getCurrentFieldsForOperation, getOperationDisplay, @@ -27,12 +21,18 @@ import { IndexPattern, IndexPatternField, DraggedField, + DataViewLensOperation, } from '../../types'; - -type GetDropProps = DatasourceDimensionDropProps & { - dragging?: DragContextState['dragging']; - groupId: string; -}; +import { + getDropPropsForSameGroup, + isOperationFromTheSameGroup, +} from '../../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; + +interface GetDropProps { + state: IndexPatternPrivateState; + source?: DragContextState['dragging']; + target: DragDropOperation; +} type DropProps = { dropTypes: DropType[]; nextLabel?: string } | undefined; @@ -41,7 +41,7 @@ const operationLabels = getOperationDisplay(); export function getNewOperation( field: IndexPatternField | undefined | false, filterOperations: (meta: OperationMetadata) => boolean, - targetColumn: GenericIndexPatternColumn, + targetColumn?: GenericIndexPatternColumn, prioritizedOperation?: GenericIndexPatternColumn['operationType'] ) { if (!field) { @@ -61,52 +61,50 @@ export function getNewOperation( return existsPrioritizedOperation ? prioritizedOperation : newOperations[0]; } -export function getField( - column: GenericIndexPatternColumn | undefined, - indexPattern: IndexPattern -) { +export function getField(column: GenericIndexPatternColumn | undefined, dataView: IndexPattern) { if (!column) { return; } - const field = (hasField(column) && indexPattern.getFieldByName(column.sourceField)) || undefined; + const field = (hasField(column) && dataView.getFieldByName(column.sourceField)) || undefined; return field; } export function getDropProps(props: GetDropProps) { - const { state, columnId, layerId, dragging, groupId, filterOperations } = props; - if (!dragging) { + const { state, source, target } = props; + if (!source) { return; } + const targetProps: DataViewLensOperation = { + ...target, + column: state.layers[target.layerId].columns[target.columnId], + dataView: state.indexPatterns[state.layers[target.layerId].indexPatternId], + }; - if (isDraggedField(dragging)) { - return getDropPropsForField({ ...props, dragging }); + if (isDraggedField(source)) { + return getDropPropsForField({ ...props, source, target: targetProps }); } - if ( - isDraggedOperation(dragging) && - dragging.layerId === layerId && - columnId !== dragging.columnId - ) { - const sourceColumn = state.layers[dragging.layerId].columns[dragging.columnId]; - const targetColumn = state.layers[layerId].columns[columnId]; - const isSameGroup = groupId === dragging.groupId; - if (isSameGroup) { - return getDropPropsForSameGroup(!targetColumn); - } - const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; - - if (filterOperations(sourceColumn)) { - return getDropPropsForCompatibleGroup( - props.dimensionGroups, - dragging.columnId, - sourceColumn, - targetColumn, - layerIndexPattern - ); - } else if (hasTheSameField(sourceColumn, targetColumn)) { + if (isOperation(source)) { + const sourceProps: DataViewLensOperation = { + ...source, + column: state.layers[source.layerId]?.columns[source.columnId], + dataView: state.indexPatterns[state.layers[source.layerId]?.indexPatternId], + }; + if (!sourceProps.column) { return; - } else { - return getDropPropsFromIncompatibleGroup({ ...props, dragging }); + } + if (target.columnId !== source.columnId && targetProps.dataView === sourceProps.dataView) { + if (isOperationFromTheSameGroup(source, target)) { + return getDropPropsForSameGroup(!targetProps.column); + } + + if (targetProps.filterOperations?.(sourceProps?.column)) { + return getDropPropsForCompatibleGroup(sourceProps, targetProps); + } else if (hasTheSameField(sourceProps.column, targetProps.column)) { + return; + } else { + return getDropPropsFromIncompatibleGroup(sourceProps, targetProps); + } } } } @@ -126,14 +124,13 @@ function hasTheSameField( function getDropPropsForField({ state, - columnId, - layerId, - dragging, - filterOperations, -}: GetDropProps & { dragging: DraggedField }): DropProps { - const targetColumn = state.layers[layerId].columns[columnId]; - const isTheSameIndexPattern = state.layers[layerId].indexPatternId === dragging.indexPatternId; - const newOperation = getNewOperation(dragging.field, filterOperations, targetColumn); + source, + target, +}: GetDropProps & { source: DraggedField }): DropProps { + const targetColumn = state.layers[target.layerId].columns[target.columnId]; + const isTheSameIndexPattern = + state.layers[target.layerId].indexPatternId === source.indexPatternId; + const newOperation = getNewOperation(source.field, target.filterOperations, targetColumn); if (isTheSameIndexPattern && newOperation) { const nextLabel = operationLabels[newOperation].displayName; @@ -141,18 +138,13 @@ function getDropPropsForField({ if (!targetColumn) { return { dropTypes: ['field_add'], nextLabel }; } else if ( - (hasField(targetColumn) && targetColumn.sourceField !== dragging.field.name) || + (hasField(targetColumn) && targetColumn.sourceField !== source.field.name) || !hasField(targetColumn) ) { - const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; + const layerDataView = state.indexPatterns[state.layers[target.layerId].indexPatternId]; return hasField(targetColumn) && - layerIndexPattern && - hasOperationSupportForMultipleFields( - layerIndexPattern, - targetColumn, - undefined, - dragging.field - ) + layerDataView && + hasOperationSupportForMultipleFields(layerDataView, targetColumn, undefined, source.field) ? { dropTypes: ['field_replace', 'field_combine'], } @@ -165,82 +157,68 @@ function getDropPropsForField({ return; } -function getDropPropsForSameGroup(isNew?: boolean): DropProps { - return !isNew ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; -} - function getDropPropsForCompatibleGroup( - dimensionGroups: VisualizationDimensionGroupConfig[], - sourceId: string, - sourceColumn?: GenericIndexPatternColumn, - targetColumn?: GenericIndexPatternColumn, - indexPattern?: IndexPattern + sourceProps: DataViewLensOperation, + targetProps: DataViewLensOperation ): DropProps { - const hasSameField = sourceColumn && hasTheSameField(sourceColumn, targetColumn); - - const canSwap = - targetColumn && - !hasSameField && - dimensionGroups - .find((group) => group.accessors.some((accessor) => accessor.columnId === sourceId)) - ?.filterOperations(targetColumn); - + if (!targetProps.column) { + return { dropTypes: ['move_compatible', 'duplicate_compatible'] }; + } + const canSwap = sourceProps.filterOperations?.(targetProps.column); const swapType: DropType[] = canSwap ? ['swap_compatible'] : []; - if (!targetColumn) { - return { dropTypes: ['move_compatible', 'duplicate_compatible', ...swapType] }; + const dropTypes: DropType[] = ['replace_compatible', 'replace_duplicate_compatible', ...swapType]; + if (!targetProps.dataView || !hasField(targetProps.column)) { + return { dropTypes }; } - if (!indexPattern || !hasField(targetColumn)) { - return { dropTypes: ['replace_compatible', 'replace_duplicate_compatible', ...swapType] }; - } - // With multi fields operations there are more combination of drops now - const dropTypes: DropType[] = []; - if (!hasSameField) { - dropTypes.push('replace_compatible', 'replace_duplicate_compatible'); - } - if (canSwap) { - dropTypes.push('swap_compatible'); - } - if (hasOperationSupportForMultipleFields(indexPattern, targetColumn, sourceColumn)) { + + if ( + hasOperationSupportForMultipleFields( + targetProps.dataView, + targetProps.column, + sourceProps.column + ) + ) { dropTypes.push('combine_compatible'); } - // return undefined if no drop action is available - if (!dropTypes.length) { - return; - } return { dropTypes, }; } -function getDropPropsFromIncompatibleGroup({ - state, - columnId, - layerId, - dragging, - filterOperations, -}: GetDropProps & { dragging: DraggedOperation }): DropProps { - const targetColumn = state.layers[layerId].columns[columnId]; - const sourceColumn = state.layers[dragging.layerId].columns[dragging.columnId]; - - const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; - if (!layerIndexPattern) { +function getDropPropsFromIncompatibleGroup( + sourceProps: DataViewLensOperation, + targetProps: DataViewLensOperation +): DropProps { + if (!targetProps.dataView || !sourceProps.column) { return; } - const sourceField = getField(sourceColumn, layerIndexPattern); - const newOperationForSource = getNewOperation(sourceField, filterOperations, targetColumn); + const sourceField = getField(sourceProps.column, sourceProps.dataView); + const newOperationForSource = getNewOperation( + sourceField, + targetProps.filterOperations, + targetProps.column + ); if (newOperationForSource) { - const targetField = getField(targetColumn, layerIndexPattern); - const canSwap = Boolean(getNewOperation(targetField, dragging.filterOperations, sourceColumn)); + const targetField = getField(targetProps.column, targetProps.dataView); + const canSwap = Boolean( + getNewOperation(targetField, sourceProps.filterOperations, sourceProps.column) + ); const dropTypes: DropType[] = []; - if (targetColumn) { + if (targetProps.column) { dropTypes.push('replace_incompatible', 'replace_duplicate_incompatible'); if (canSwap) { dropTypes.push('swap_incompatible'); } - if (hasOperationSupportForMultipleFields(layerIndexPattern, targetColumn, sourceColumn)) { + if ( + hasOperationSupportForMultipleFields( + targetProps.dataView, + targetProps.column, + sourceProps.column + ) + ) { dropTypes.push('combine_incompatible'); } } else { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts new file mode 100644 index 0000000000000..40121cf99f546 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts @@ -0,0 +1,292 @@ +/* + * 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 { IndexPattern, IndexPatternLayer } from '../../types'; +import { documentField } from '../../document_field'; +import { OperationMetadata } from '../../../types'; +import { + DateHistogramIndexPatternColumn, + GenericIndexPatternColumn, + StaticValueIndexPatternColumn, + TermsIndexPatternColumn, +} from '../../operations'; +import { getFieldByNameFactory } from '../../pure_helpers'; +jest.mock('../../../id_generator'); + +export const mockDataViews = (): Record => { + const 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, + }, + { + name: 'src', + displayName: 'src', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + documentField, + ]; + return { + first: { + id: 'first', + title: 'first', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields, + getFieldByName: getFieldByNameFactory(fields), + }, + second: { + id: 'second', + title: 'my-fake-restricted-pattern', + hasRestrictions: true, + timeFieldName: 'timestamp', + fields: [fields[0]], + getFieldByName: getFieldByNameFactory([fields[0]]), + }, + }; +}; + +export const mockedColumns: Record = { + count: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + staticValue: { + label: 'Static value: 0.75', + dataType: 'number', + operationType: 'static_value', + isStaticValue: true, + isBucketed: false, + scale: 'ratio', + params: { + value: '0.75', + }, + references: [], + } as StaticValueIndexPatternColumn, + dateHistogram: { + label: 'Date histogram of timestamp', + customLabel: true, + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + } as DateHistogramIndexPatternColumn, + dateHistogramCopy: { + label: 'Date histogram of timestamp (1)', + customLabel: true, + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + } as DateHistogramIndexPatternColumn, + terms: { + label: 'Top 10 values of src', + dataType: 'string', + isBucketed: true, + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + } as TermsIndexPatternColumn, + terms2: { + label: 'Top 10 values of dest', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'dest', + } as TermsIndexPatternColumn, + sum: { + label: 'Sum of bytes', + dataType: 'number', + isBucketed: false, + operationType: 'sum', + sourceField: 'bytes', + } as GenericIndexPatternColumn, + median: { + label: 'Median of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'median', + sourceField: 'bytes', + } as GenericIndexPatternColumn, + uniqueCount: { + label: 'Unique count of bytes', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'unique_count', + } as GenericIndexPatternColumn, +}; + +export const mockedLayers: Record IndexPatternLayer> = { + singleColumnLayer: (id = 'col1') => ({ + indexPatternId: 'first', + columnOrder: [id], + columns: { + [id]: mockedColumns.dateHistogram, + }, + incompleteColumns: {}, + }), + doubleColumnLayer: (id1 = 'col1', id2 = 'col2') => ({ + indexPatternId: 'first', + columnOrder: [id1, id2], + columns: { + [id1]: mockedColumns.dateHistogram, + [id2]: mockedColumns.terms, + }, + incompleteColumns: {}, + }), + multipleColumnsLayer: (id1 = 'col1', id2 = 'col2', id3 = 'col3', id4 = 'col4') => ({ + indexPatternId: 'first', + columnOrder: [id1, id2, id3, id4], + columns: { + [id1]: mockedColumns.dateHistogram, + [id2]: mockedColumns.terms, + [id3]: mockedColumns.terms2, + [id4]: mockedColumns.median, + }, + }), + emptyLayer: () => ({ + indexPatternId: 'first', + columnOrder: [], + columns: {}, + }), +}; + +export const mockedDraggedField = { + field: { type: 'number', name: 'bytes', aggregatable: true }, + indexPatternId: 'first', + id: 'bar', + humanData: { label: 'Label' }, +}; + +export const mockedDndOperations = { + notFiltering: { + layerId: 'first', + groupId: 'a', + filterOperations: () => true, + columnId: 'col1', + id: 'col1', + humanData: { label: 'Column 1' }, + }, + metric: { + layerId: 'first', + groupId: 'a', + columnId: 'col1', + filterOperations: (op: OperationMetadata) => !op.isBucketed, + id: 'col1', + humanData: { label: 'Column 1' }, + }, + numericalOnly: { + layerId: 'first', + groupId: 'a', + columnId: 'col1', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + id: 'col1', + humanData: { label: 'Column 1' }, + }, + bucket: { + columnId: 'col2', + groupId: 'b', + layerId: 'first', + id: 'col2', + humanData: { label: 'Column 2' }, + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + staticValue: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Column 2' }, + filterOperations: (op: OperationMetadata) => !!op.isStaticValue, + }, + bucket2: { + columnId: 'col3', + groupId: 'b', + layerId: 'first', + id: 'col3', + humanData: { + label: '', + }, + }, + metricC: { + columnId: 'col4', + groupId: 'c', + layerId: 'first', + id: 'col4', + humanData: { + label: '', + }, + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, +}; 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 new file mode 100644 index 0000000000000..b959256a86303 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts @@ -0,0 +1,2257 @@ +/* + * 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 { onDrop } from './on_drop_handler'; +import { IndexPatternPrivateState } from '../../types'; +import { OperationMetadata, DropType, DatasourceDimensionDropHandlerProps } from '../../../types'; +import { FormulaIndexPatternColumn, MedianIndexPatternColumn } from '../../operations'; +import { generateId } from '../../../id_generator'; +import { + mockDataViews, + mockedLayers, + mockedDraggedField, + mockedDndOperations, + mockedColumns, +} from './mocks'; + +jest.mock('../../../id_generator'); + +const dimensionGroups = [ + { + accessors: [], + groupId: 'a', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + { + accessors: [{ columnId: 'col1' }, { columnId: 'col2' }, { columnId: 'col3' }], + groupId: 'b', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + { + accessors: [{ columnId: 'col4' }], + groupId: 'c', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, +]; + +function getStateWithMultiFieldColumn(state: IndexPatternPrivateState) { + return { + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columns: { + col1: mockedColumns.terms2, + }, + }, + }, + }; +} + +describe('IndexPatternDimensionEditorPanel: onDrop', () => { + let state: IndexPatternPrivateState; + let setState: jest.Mock; + let defaultProps: DatasourceDimensionDropHandlerProps; + + beforeEach(() => { + state = { + indexPatternRefs: [], + indexPatterns: mockDataViews(), + currentIndexPatternId: 'first', + isFirstExistenceFetch: false, + existingFields: { + first: { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + layers: { + first: mockedLayers.singleColumnLayer(), + second: mockedLayers.emptyLayer(), + }, + }; + + setState = jest.fn(); + + defaultProps = { + dropType: 'reorder', + source: { name: 'bar', id: 'bar', humanData: { label: 'Label' } }, + target: mockedDndOperations.notFiltering, + state, + setState, + dimensionGroups: [], + }; + + jest.clearAllMocks(); + }); + + describe('dropping a field', () => { + it('updates a column when a field is dropped', () => { + onDrop({ + ...defaultProps, + source: mockedDraggedField, + dropType: 'field_replace', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + }), + }), + }, + }); + }); + it('selects the specific operation that was valid on drop', () => { + onDrop({ + ...defaultProps, + source: mockedDraggedField, + dropType: 'field_replace', + target: { + ...defaultProps.target, + filterOperations: (op: OperationMetadata) => op.isBucketed, + columnId: 'col2', + }, + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + }, + }, + }, + }); + }); + it('keeps the operation when dropping a different compatible field', () => { + onDrop({ + ...defaultProps, + source: { + humanData: { label: 'Label1' }, + field: { name: 'memory', type: 'number', aggregatable: true }, + indexPatternId: 'first', + id: '1', + }, + state: { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: 'first', + columnOrder: ['col1'], + columns: { + col1: mockedColumns.sum, + }, + }, + }, + }, + dropType: 'field_replace', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + operationType: 'sum', + dataType: 'number', + sourceField: 'memory', + }), + }), + }), + }, + }); + }); + it('appends the dropped column when a field is dropped', () => { + onDrop({ + ...defaultProps, + source: mockedDraggedField, + dropType: 'field_replace', + target: { + ...defaultProps.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }, + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + }, + }, + }, + }); + }); + it('dimensionGroups are defined - appends the dropped column in the right place when a field is dropped', () => { + const testState = { ...state }; + testState.layers.first = { ...mockedLayers.multipleColumnsLayer() }; + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + + onDrop({ + ...defaultProps, + source: mockedDraggedField, + dimensionGroups, + dropType: 'field_add', + target: { + ...defaultProps.target, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', + columnId: 'newCol', + }, + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('appends the new field to the column that supports multiple fields when a field is dropped', () => { + state = getStateWithMultiFieldColumn(state); + onDrop({ + ...defaultProps, + state, + source: mockedDraggedField, + dropType: 'field_combine', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'string', + sourceField: 'dest', + params: expect.objectContaining({ secondaryFields: ['bytes'] }), + }), + }), + }), + }, + }); + }); + }); + + describe('dropping a dimension', () => { + it('sets correct order in group for metric and bucket columns when duplicating a column in group', () => { + const testState: IndexPatternPrivateState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: 'first', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: mockedColumns.dateHistogram, + col2: mockedColumns.terms, + col3: mockedColumns.sum, + }, + }, + }, + }; + + const referenceDragging = { + columnId: 'col3', + groupId: 'a', + layerId: 'first', + id: 'col3', + humanData: { label: 'Label' }, + }; + + onDrop({ + ...defaultProps, + source: referenceDragging, + state: testState, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'newCol', + }, + }); + // metric is appended + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'newCol'], + columns: { + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + newCol: testState.layers.first.columns.col3, + }, + }, + }, + }); + + const bucketDragging = { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + humanData: { label: 'Label' }, + }; + + onDrop({ + ...defaultProps, + state: testState, + dropType: 'duplicate_compatible', + source: bucketDragging, + target: { + ...defaultProps.target, + columnId: 'newCol', + }, + }); + + // bucket is placed after the last existing bucket + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'newCol', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + newCol: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + }, + }, + }); + }); + + it('when duplicating fullReference column, the referenced columns get duplicated too', () => { + (generateId as jest.Mock).mockReturnValue(`ref1Copy`); + const testState: IndexPatternPrivateState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: '1', + columnOrder: ['col1', 'ref1'], + columns: { + col1: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['ref1'], + }, + ref1: mockedColumns.count, + }, + }, + }, + }; + const referenceDragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + onDrop({ + ...defaultProps, + source: referenceDragging, + state: testState, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'col1Copy', + }, + }); + + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'ref1', 'ref1Copy', 'col1Copy'], + columns: { + ref1: testState.layers.first.columns.ref1, + col1: testState.layers.first.columns.col1, + ref1Copy: { ...testState.layers.first.columns.ref1 }, + col1Copy: { + ...testState.layers.first.columns.col1, + references: ['ref1Copy'], + }, + }, + }, + }, + }); + }); + + it('when duplicating fullReference column, the multiple referenced columns get duplicated too', () => { + (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`); + (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`); + const testState: IndexPatternPrivateState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: '1', + columnOrder: ['col1', 'ref1'], + columns: { + col1: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['ref1', 'ref2'], + }, + ref1: mockedColumns.count, + ref2: mockedColumns.uniqueCount, + }, + }, + }, + }; + const metricDragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + onDrop({ + ...defaultProps, + source: metricDragging, + state: testState, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'col1Copy', + }, + }); + + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'ref1', 'ref2', 'ref1Copy', 'ref2Copy', 'col1Copy'], + columns: { + ref1: testState.layers.first.columns.ref1, + ref2: testState.layers.first.columns.ref2, + col1: testState.layers.first.columns.col1, + ref2Copy: { ...testState.layers.first.columns.ref2 }, + ref1Copy: { ...testState.layers.first.columns.ref1 }, + col1Copy: { + ...testState.layers.first.columns.col1, + references: ['ref1Copy', 'ref2Copy'], + }, + }, + }, + }, + }); + }); + + it('when duplicating fullReference column, the referenced columns get duplicated', () => { + (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`); + (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`); + const testState: IndexPatternPrivateState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: '1', + columnOrder: ['ref2', 'ref1', 'col1'], + columns: { + col1: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['ref1', 'ref2'], + }, + ref1: mockedColumns.count, + ref2: { + label: 'Unique count of bytes', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'unique_count', + }, + }, + }, + }, + }; + const refDragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + onDrop({ + ...defaultProps, + source: refDragging, + state: testState, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'col1Copy', + }, + }); + + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['ref2', 'ref1', 'col1', 'ref1Copy', 'ref2Copy', 'col1Copy'], + columns: { + ref1: testState.layers.first.columns.ref1, + ref2: testState.layers.first.columns.ref2, + col1: testState.layers.first.columns.col1, + ref2Copy: { ...testState.layers.first.columns.ref2 }, + ref1Copy: { + ...testState.layers.first.columns.ref1, + }, + col1Copy: { + ...testState.layers.first.columns.col1, + references: ['ref1Copy', 'ref2Copy'], + }, + }, + }, + }, + }); + }); + + it('sets correct order in group when reordering a column in group', () => { + const testState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: 'first', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: mockedColumns.dateHistogram, + col2: mockedColumns.terms, + col3: mockedColumns.terms2, + }, + }, + }, + }; + + const defaultReorderDropParams = { + ...defaultProps, + target: { + ...defaultProps.target, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }, + source: mockedDndOperations.metric, + state: testState, + dropType: 'reorder' as DropType, + }; + + const stateWithColumnOrder = (columnOrder: string[]) => { + return { + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder, + columns: { + ...testState.layers.first.columns, + }, + }, + }, + }; + }; + + // first element to last + onDrop({ + ...defaultReorderDropParams, + target: { + ...defaultReorderDropParams.target, + columnId: 'col3', + }, + }); + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col3', 'col1'])); + + // last element to first + onDrop({ + ...defaultReorderDropParams, + target: { + ...defaultReorderDropParams.target, + columnId: 'col1', + }, + source: { + humanData: { label: 'Label1' }, + columnId: 'col3', + groupId: 'a', + layerId: 'first', + id: 'col3', + }, + }); + expect(setState).toBeCalledTimes(2); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col3', 'col1', 'col2'])); + + // middle column to first + onDrop({ + ...defaultReorderDropParams, + target: { + ...defaultReorderDropParams.target, + columnId: 'col1', + }, + source: { + humanData: { label: 'Label1' }, + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + }, + }); + expect(setState).toBeCalledTimes(3); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col1', 'col3'])); + + // middle column to last + onDrop({ + ...defaultReorderDropParams, + target: { + ...defaultReorderDropParams.target, + columnId: 'col3', + }, + source: { + humanData: { label: 'Label1' }, + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + }, + }); + expect(setState).toBeCalledTimes(4); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col1', 'col3', 'col2'])); + }); + + it('updates the column id when moving an operation to an empty dimension', () => { + onDrop({ + ...defaultProps, + source: mockedDndOperations.metric, + target: { + ...defaultProps.target, + columnId: 'col2', + }, + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columnOrder: ['col2'], + columns: { + col2: state.layers.first.columns.col1, + }, + }, + }, + }); + }); + + it('replaces an operation when moving to a populated dimension', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'first', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: mockedColumns.terms, + col3: mockedColumns.count, + }, + }; + + onDrop({ + ...defaultProps, + source: mockedDndOperations.bucket, + state: testState, + dropType: 'replace_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['col1', 'col3'], + columns: { + col1: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + }, + }, + }); + }); + + it('when combine compatible columns should append dropped column fields into the target one', () => { + state = getStateWithMultiFieldColumn(state); + state.layers.first.columns = { + ...state.layers.first.columns, + col2: mockedColumns.terms, + }; + onDrop({ + ...defaultProps, + state, + source: { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + humanData: { label: 'Label' }, + }, + dropType: 'combine_compatible', + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'string', + sourceField: 'dest', + params: expect.objectContaining({ secondaryFields: ['src'] }), + }), + }), + }), + }, + }); + }); + + describe('dimension group aware ordering and copying', () => { + let testState: IndexPatternPrivateState; + beforeEach(() => { + testState = { ...state }; + testState.layers.first = { ...mockedLayers.multipleColumnsLayer() }; + }); + + it('respects groups on moving operations between compatible groups', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging col2 into newCol in group a + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + source: mockedDndOperations.bucket, + state: testState, + dimensionGroups, + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['newCol', 'col1', 'col3', 'col4'], + columns: { + newCol: testState.layers.first.columns.col2, + col1: testState.layers.first.columns.col1, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on duplicating operations between compatible groups', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging col2 into newCol in group a + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + source: mockedDndOperations.bucket, + state: testState, + dimensionGroups, + dropType: 'duplicate_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: testState.layers.first.columns.col2, + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on moving operations between compatible groups with overwrite', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // dragging col3 onto col1 in group a + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + }, + source: mockedDndOperations.bucket2, + state: testState, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['col1', 'col2', 'col4'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on moving operations if some columns are not listed in groups', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // col5, col6 not in visualization groups + // dragging col3 onto col1 in group a + onDrop({ + ...defaultProps, + source: mockedDndOperations.bucket2, + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + }, + state: { + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'], + columns: { + ...testState.layers.first.columns, + col5: { + dataType: 'number', + operationType: 'count', + label: '', + isBucketed: false, + sourceField: '___records___', + customLabel: true, + }, + col6: { + dataType: 'number', + operationType: 'count', + label: '', + isBucketed: false, + sourceField: '___records___', + customLabel: true, + }, + }, + }, + }, + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['col1', 'col2', 'col4', 'col5', 'col6'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col4: testState.layers.first.columns.col4, + col5: expect.objectContaining({ + dataType: 'number', + operationType: 'count', + label: '', + isBucketed: false, + sourceField: '___records___', + }), + col6: expect.objectContaining({ + dataType: 'number', + operationType: 'count', + label: '', + isBucketed: false, + sourceField: '___records___', + }), + }, + }, + }, + }); + }); + + it('respects groups on duplicating operations between compatible groups with overwrite', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // dragging col3 onto col1 in group a + + onDrop({ + ...defaultProps, + source: mockedDndOperations.bucket2, + state: testState, + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'duplicate_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('moves newly created dimension to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col1 into newCol in group b + onDrop({ + ...defaultProps, + dropType: 'move_compatible', + source: mockedDndOperations.metric, + state: testState, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'b', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['col2', 'col3', 'newCol', 'col4'], + columns: { + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('copies column to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // copying col1 within group a + onDrop({ + ...defaultProps, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + source: mockedDndOperations.metric, + state: testState, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('appends the dropped column in the right place respecting custom nestingOrder', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + + onDrop({ + ...defaultProps, + source: mockedDraggedField, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }, + dimensionGroups: [ + // a and b are ordered in reverse visually, but nesting order keeps them in place for column order + { ...dimensionGroups[1], nestingOrder: 1 }, + { ...dimensionGroups[0], nestingOrder: 0 }, + { ...dimensionGroups[2] }, + ], + dropType: 'field_add', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('moves incompatible column to the bottom of the target group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into newCol in group a + + onDrop({ + ...defaultProps, + dropType: 'move_incompatible', + source: mockedDndOperations.metricC, + state: testState, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: expect.objectContaining({ + sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) + .sourceField, + }), + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('copies incompatible column to the bottom of the target group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into newCol in group a + + onDrop({ + ...defaultProps, + dropType: 'duplicate_incompatible', + source: mockedDndOperations.metricC, + state: testState, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: expect.objectContaining({ + sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) + .sourceField, + }), + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('moves incompatible column with overwrite keeping order of target column', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col2 in group b + + onDrop({ + ...defaultProps, + dropType: 'move_incompatible', + source: mockedDndOperations.metricC, + state: testState, + target: { + ...defaultProps.target, + columnId: 'col2', + groupId: 'b', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: { + isBucketed: true, + label: 'Top 10 values of bytes', + operationType: 'terms', + sourceField: 'bytes', + dataType: 'number', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + size: 10, + parentFormat: { id: 'terms' }, + }, + }, + col3: testState.layers.first.columns.col3, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('when swapping compatibly, columns carry order', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col1 + + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + }, + source: mockedDndOperations.metricC, + dropType: 'swap_compatible', + state: testState, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col4, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col1, + }, + }, + }, + }); + }); + + it('when swapping incompatibly, newly created columns take order from the columns they replace', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col2 + + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'col2', + groupId: 'b', + }, + dropType: 'swap_incompatible', + source: mockedDndOperations.metricC, + state: testState, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + col2: { + isBucketed: true, + label: 'Top 10 values of bytes', + operationType: 'terms', + sourceField: 'bytes', + dataType: 'number', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + parentFormat: { id: 'terms' }, + size: 10, + }, + }, + col3: testState.layers.first.columns.col3, + col4: { + isBucketed: false, + label: 'Unique count of src', + filter: undefined, + operationType: 'unique_count', + sourceField: 'src', + timeShift: undefined, + dataType: 'number', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + }, + }, + incompleteColumns: {}, + }, + }, + }); + }); + }); + + describe('onDrop between layers', () => { + const defaultDimensionGroups = [ + { + groupId: 'x', + groupLabel: 'Horizontal axis', + accessors: [], + supportsMoreColumns: true, + dataTestSubj: 'lnsXY_xDimensionPanel', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + { + groupId: 'y', + groupLabel: 'Vertical axis', + accessors: [], + supportsMoreColumns: true, + required: true, + dataTestSubj: 'lnsXY_yDimensionPanel', + enableDimensionEditor: true, + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + { + groupId: 'breakdown', + groupLabel: 'Break down by', + accessors: [], + supportsMoreColumns: true, + dataTestSubj: 'lnsXY_splitDimensionPanel', + required: false, + enableDimensionEditor: true, + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + ]; + describe('simple operations', () => { + let props: DatasourceDimensionDropHandlerProps; + beforeEach(() => { + setState = jest.fn(); + + props = { + state: { + indexPatternRefs: [], + indexPatterns: mockDataViews(), + currentIndexPatternId: 'first', + isFirstExistenceFetch: false, + existingFields: { + first: { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + layers: { + first: mockedLayers.singleColumnLayer(), + second: mockedLayers.multipleColumnsLayer('col2', 'col3', 'col4', 'col5'), + }, + }, + setState: jest.fn(), + source: { + id: 'col1', + humanData: { label: '2' }, + columnId: 'col1', + groupId: 'x', + layerId: 'first', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + target: { + filterOperations: (op: OperationMetadata) => op.isBucketed, + columnId: 'newCol', + groupId: 'x', + layerId: 'second', + }, + dimensionGroups: defaultDimensionGroups, + dropType: 'move_compatible', + }; + jest.clearAllMocks(); + }); + it('doesnt allow dropping for different data views', () => { + props.state.layers.second.indexPatternId = 'second'; + expect(onDrop(props)).toEqual(false); + expect(props.setState).not.toHaveBeenCalled(); + }); + it('move_compatible; allows dropping to the compatible group in different layer to empty column', () => { + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'newCol', 'col5'], + columns: { + ...props.state.layers.second.columns, + newCol: mockedColumns.dateHistogram, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('duplicate_compatible: allows dropping to the compatible group in different layer to empty column', () => { + expect(onDrop({ ...props, dropType: 'duplicate_compatible' })).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + second: { + columnOrder: ['col2', 'col3', 'col4', 'newCol', 'col5'], + columns: { + ...props.state.layers.second.columns, + newCol: mockedColumns.dateHistogram, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('swap_compatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + + target: { + ...props.target, + columnId: 'col4', + groupId: 'breakdown', + layerId: 'second', + }, + dropType: 'swap_compatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...props.state.layers.first, + columns: { + col1: props.state.layers.second.columns.col4, + }, + }, + second: { + ...props.state.layers.second, + columns: { + ...props.state.layers.second.columns, + col4: props.state.layers.first.columns.col1, + }, + }, + }, + }); + }); + it('replace_compatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col4', + groupId: 'breakdown', + layerId: 'second', + }, + dropType: 'replace_compatible', + }; + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col4: mockedColumns.dateHistogram, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('replace_duplicate_compatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col4', + groupId: 'breakdown', + layerId: 'second', + }, + dropType: 'replace_duplicate_compatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col4: mockedColumns.dateHistogram, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('replace_duplicate_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col5', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'replace_duplicate_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col5: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('replace_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col5', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'replace_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col5: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('move_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'newCol', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'move_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5', 'newCol'], + columns: { + ...props.state.layers.second.columns, + newCol: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('duplicate_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'newCol', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'duplicate_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5', 'newCol'], + columns: { + ...props.state.layers.second.columns, + newCol: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('swap_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col5', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'swap_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col1: { + dataType: 'number', + isBucketed: true, + label: 'bytes', + operationType: 'range', + params: { + includeEmptyRows: true, + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'bytes', + }, + }, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col5: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('combine_compatible: allows dropping to combine to multiterms', () => { + onDrop({ + ...props, + state: { + ...props.state, + layers: { + ...props.state.layers, + first: { + ...props.state.layers.first, + columns: { + terms1: mockedColumns.terms, + }, + }, + }, + }, + source: { + columnId: 'terms1', + groupId: 'a', + layerId: 'first', + id: 'terms1', + humanData: { label: 'Label' }, + }, + dropType: 'combine_compatible', + target: { + ...props.target, + columnId: 'col4', + groupId: 'a', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + }); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { ...mockedLayers.emptyLayer(), incompleteColumns: {} }, + second: { + ...props.state.layers.second, + incompleteColumns: {}, + columns: { + ...props.state.layers.second.columns, + col4: { + dataType: 'string', + isBucketed: true, + label: 'Top values of dest + 1 other', + operationType: 'terms', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + parentFormat: { + id: 'multi_terms', + }, + secondaryFields: ['src'], + size: 10, + }, + sourceField: 'dest', + }, + }, + }, + }, + }); + }); + it('combine_incompatible: allows dropping to combine to multiterms', () => { + onDrop({ + ...props, + state: { + ...props.state, + layers: { + ...props.state.layers, + first: { + ...props.state.layers.first, + columns: { + median: mockedColumns.median, + }, + }, + }, + }, + source: { + columnId: 'median', + groupId: 'x', + layerId: 'first', + id: 'median', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + dropType: 'combine_incompatible', + target: { + ...props.target, + columnId: 'col4', + groupId: 'breakdown', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + }); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { ...mockedLayers.emptyLayer(), incompleteColumns: {} }, + second: { + ...props.state.layers.second, + incompleteColumns: {}, + columns: { + ...props.state.layers.second.columns, + col4: { + dataType: 'string', + isBucketed: true, + label: 'Top values of dest + 1 other', + operationType: 'terms', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + parentFormat: { + id: 'multi_terms', + }, + secondaryFields: ['bytes'], + size: 10, + }, + sourceField: 'dest', + }, + }, + }, + }, + }); + }); + }); + describe('references', () => { + let props: DatasourceDimensionDropHandlerProps; + beforeEach(() => { + props = { + dimensionGroups: defaultDimensionGroups, + setState: jest.fn(), + dropType: 'move_compatible', + + state: { + layers: { + first: { + indexPatternId: 'first', + columns: { + firstColumnX0: { + label: 'Part of count()', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + customLabel: true, + }, + firstColumn: { + label: 'count()', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { formula: 'count()' }, + references: ['firstColumnX0'], + } as FormulaIndexPatternColumn, + }, + columnOrder: ['firstColumn', 'firstColumnX0'], + incompleteColumns: {}, + }, + second: { + indexPatternId: 'first', + columns: { + secondX0: { + label: 'Part of count()', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + customLabel: true, + }, + second: { + label: 'count()', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { formula: 'count()' }, + references: ['secondX0'], + } as FormulaIndexPatternColumn, + }, + columnOrder: ['second', 'secondX0'], + }, + }, + indexPatternRefs: [], + indexPatterns: mockDataViews(), + currentIndexPatternId: 'first', + isFirstExistenceFetch: false, + existingFields: { + first: { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + }, + source: { + columnId: 'firstColumn', + groupId: 'y', + layerId: 'first', + id: 'firstColumn', + humanData: { + label: 'count()', + }, + }, + target: { + columnId: 'newColumn', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + }; + + jest.clearAllMocks(); + }); + + it('move_compatible; allows dropping to the compatible group in different layer to empty column', () => { + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['second', 'secondX0', 'newColumnX0', 'newColumn'], + columns: { + ...props.state.layers.second.columns, + newColumn: { + dataType: 'number', + isBucketed: false, + label: 'count()', + operationType: 'formula', + params: { + formula: 'count()', + isFormulaBroken: false, + }, + references: ['newColumnX0'], + scale: 'ratio', + }, + newColumnX0: { + customLabel: true, + dataType: 'number', + filter: undefined, + isBucketed: false, + label: 'Part of count()', + operationType: 'count', + params: { + emptyAsNull: false, + }, + scale: 'ratio', + sourceField: '___records___', + timeScale: undefined, + timeShift: undefined, + }, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('replace_compatible: allows dropping to compatible group to replace an existing column', () => { + expect( + onDrop({ + ...props, + target: { + columnId: 'second', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + }) + ).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['second', 'secondX0'], + columns: { + ...props.state.layers.second.columns, + second: { + dataType: 'number', + isBucketed: false, + label: 'count()', + operationType: 'formula', + params: { + formula: 'count()', + isFormulaBroken: false, + }, + references: ['secondX0'], + scale: 'ratio', + }, + secondX0: { + customLabel: true, + dataType: 'number', + filter: undefined, + isBucketed: false, + label: 'Part of count()', + operationType: 'count', + params: { + emptyAsNull: false, + }, + scale: 'ratio', + sourceField: '___records___', + timeScale: undefined, + timeShift: undefined, + }, + }, + indexPatternId: 'first', + }, + }, + }); + }); + }); + }); + }); +}); 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 c9e806050caad..950db5fec4a67 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,7 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { DatasourceDimensionDropHandlerProps, DraggedOperation } from '../../../types'; +import { + DatasourceDimensionDropHandlerProps, + DragDropOperation, + DropType, + isOperation, + StateSetter, + VisualizationDimensionGroupConfig, +} from '../../../types'; import { insertOrReplaceColumn, deleteColumn, @@ -14,160 +21,128 @@ import { hasOperationSupportForMultipleFields, getOperationHelperForMultipleFields, replaceColumn, + deleteColumnInLayers, } from '../../operations'; -import { mergeLayer } from '../../state_helpers'; +import { mergeLayer, mergeLayers } from '../../state_helpers'; import { isDraggedField } from '../../pure_utils'; import { getNewOperation, getField } from './get_drop_props'; -import { IndexPatternPrivateState, DraggedField } from '../../types'; +import { IndexPatternPrivateState, DraggedField, DataViewLensOperation } from '../../types'; import { trackUiEvent } from '../../../lens_ui_telemetry'; -type DropHandlerProps = DatasourceDimensionDropHandlerProps & { - droppedItem: T; -}; +interface DropHandlerProps { + state: IndexPatternPrivateState; + setState: StateSetter< + IndexPatternPrivateState, + { + isDimensionComplete?: boolean; + forceRender?: boolean; + } + >; + dimensionGroups: VisualizationDimensionGroupConfig[]; + dropType?: DropType; + source: T; + target: DataViewLensOperation; +} export function onDrop(props: DatasourceDimensionDropHandlerProps) { - const { droppedItem, dropType } = props; + const { target, source, dropType, state } = props; - if (dropType === 'field_add' || dropType === 'field_replace' || dropType === 'field_combine') { - return operationOnDropMap[dropType]({ - ...props, - droppedItem: droppedItem as DraggedField, - }); + if (!source) { + return false; + } + if (isDraggedField(source) && isFieldDropType(dropType)) { + return onFieldDrop( + { + ...props, + target: { + ...target, + dataView: state.indexPatterns[state.layers[target.layerId].indexPatternId], + }, + source, + }, + dropType === 'field_combine' + ); } - return operationOnDropMap[dropType]({ - ...props, - droppedItem: droppedItem as DraggedOperation, - }); -} - -const operationOnDropMap = { - field_add: onFieldDrop, - field_replace: onFieldDrop, - field_combine: (props: DropHandlerProps) => onFieldDrop(props, true), - - reorder: onReorder, - - move_compatible: (props: DropHandlerProps) => onMoveCompatible(props, true), - replace_compatible: (props: DropHandlerProps) => onMoveCompatible(props, true), - duplicate_compatible: onMoveCompatible, - replace_duplicate_compatible: onMoveCompatible, - - move_incompatible: (props: DropHandlerProps) => onMoveIncompatible(props, true), - replace_incompatible: (props: DropHandlerProps) => - onMoveIncompatible(props, true), - duplicate_incompatible: onMoveIncompatible, - replace_duplicate_incompatible: onMoveIncompatible, - - swap_compatible: onSwapCompatible, - swap_incompatible: onSwapIncompatible, - combine_compatible: onCombineCompatible, - combine_incompatible: onCombineCompatible, -}; - -function onCombineCompatible({ - columnId, - setState, - state, - layerId, - droppedItem, - dimensionGroups, - groupId, -}: DropHandlerProps) { - const layer = state.layers[layerId]; - const sourceId = droppedItem.columnId; - const targetId = columnId; - const indexPattern = state.indexPatterns[layer.indexPatternId]; - const sourceColumn = layer.columns[sourceId]; - const targetColumn = layer.columns[targetId]; - // extract the field from the source column - const sourceField = getField(sourceColumn, indexPattern); - const targetField = getField(targetColumn, indexPattern); - if (!sourceField || !targetField) { + if (!isOperation(source)) { + return false; + } + const sourceDataView = state.indexPatterns[state.layers[source.layerId].indexPatternId]; + const targetDataView = state.indexPatterns[state.layers[target.layerId].indexPatternId]; + if (sourceDataView !== targetDataView) { return false; } - // pass it to the target column and delete the source column - const initialParams = { - params: - getOperationHelperForMultipleFields(targetColumn.operationType)?.({ - targetColumn, - sourceColumn, - indexPattern, - }) ?? {}, - }; - - const modifiedLayer = replaceColumn({ - layer, - columnId, - indexPattern, - op: targetColumn.operationType, - field: targetField, - visualizationGroups: dimensionGroups, - targetGroup: groupId, - initialParams, - shouldCombineField: true, - }); - const newLayer = deleteColumn({ - layer: modifiedLayer, - columnId: sourceId, - indexPattern, - }); - // Time to replace - setState( - mergeLayer({ - state, - layerId, - newLayer, - }) - ); + const operationProps = { + ...props, + target: { + ...target, + dataView: targetDataView, + }, + source: { + ...source, + dataView: sourceDataView, + }, + }; + if (dropType === 'reorder') { + return onReorder(operationProps); + } - return { deleted: sourceId }; + if (['move_compatible', 'replace_compatible'].includes(dropType)) { + return onMoveCompatible(operationProps, true); + } + if (['duplicate_compatible', 'replace_duplicate_compatible'].includes(dropType)) { + return onMoveCompatible(operationProps); + } + if (['move_incompatible', 'replace_incompatible'].includes(dropType)) { + return onMoveIncompatible(operationProps, true); + } + if (['duplicate_incompatible', 'replace_duplicate_incompatible'].includes(dropType)) { + return onMoveIncompatible(operationProps); + } + if (dropType === 'swap_compatible') { + return onSwapCompatible(operationProps); + } + if (dropType === 'swap_incompatible') { + return onSwapIncompatible(operationProps); + } + if (['combine_incompatible', 'combine_compatible'].includes(dropType)) { + return onCombine(operationProps); + } } +const isFieldDropType = (dropType: DropType) => + ['field_add', 'field_replace', 'field_combine'].includes(dropType); + function onFieldDrop(props: DropHandlerProps, shouldAddField?: boolean) { - const { - columnId, - setState, - state, - layerId, - droppedItem, - filterOperations, - groupId, - dimensionGroups, - } = props; + const { setState, state, source, target, dimensionGroups } = props; const prioritizedOperation = dimensionGroups.find( - (g) => g.groupId === groupId + (g) => g.groupId === target.groupId )?.prioritizedOperation; - const layer = state.layers[layerId]; + const layer = state.layers[target.layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; - const targetColumn = layer.columns[columnId]; + const targetColumn = layer.columns[target.columnId]; const newOperation = shouldAddField ? targetColumn.operationType - : getNewOperation(droppedItem.field, filterOperations, targetColumn, prioritizedOperation); + : getNewOperation(source.field, target.filterOperations, targetColumn, prioritizedOperation); if ( - !isDraggedField(droppedItem) || + !isDraggedField(source) || !newOperation || (shouldAddField && - !hasOperationSupportForMultipleFields( - indexPattern, - targetColumn, - undefined, - droppedItem.field - )) + !hasOperationSupportForMultipleFields(indexPattern, targetColumn, undefined, source.field)) ) { return false; } - const field = shouldAddField ? getField(targetColumn, indexPattern) : droppedItem.field; + const field = shouldAddField ? getField(targetColumn, indexPattern) : source.field; const initialParams = shouldAddField ? { params: getOperationHelperForMultipleFields(targetColumn.operationType)?.({ targetColumn, - field: droppedItem.field, + field: source.field, indexPattern, }) || {}, } @@ -175,12 +150,12 @@ function onFieldDrop(props: DropHandlerProps, shouldAddField?: boo const newLayer = insertOrReplaceColumn({ layer, - columnId, + columnId: target.columnId, indexPattern, op: newOperation, field, visualizationGroups: dimensionGroups, - targetGroup: groupId, + targetGroup: target.groupId, shouldCombineField: shouldAddField, initialParams, }); @@ -188,82 +163,71 @@ function onFieldDrop(props: DropHandlerProps, shouldAddField?: boo trackUiEvent('drop_onto_dimension'); const hasData = Object.values(state.layers).some(({ columns }) => columns.length); trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); - setState(mergeLayer({ state, layerId, newLayer })); + setState(mergeLayer({ state, layerId: target.layerId, newLayer })); return true; } function onMoveCompatible( - { - columnId, - setState, - state, - layerId, - droppedItem, - dimensionGroups, - groupId, - }: DropHandlerProps, + { setState, state, source, target, dimensionGroups }: DropHandlerProps, shouldDeleteSource?: boolean ) { - const layer = state.layers[layerId]; - const sourceColumn = layer.columns[droppedItem.columnId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; - - const modifiedLayer = copyColumn({ - layer, - targetId: columnId, - sourceColumnId: droppedItem.columnId, - sourceColumn, + const modifiedLayers = copyColumn({ + layers: state.layers, + target, + source, shouldDeleteSource, - indexPattern, }); - const updatedColumnOrder = reorderByGroups( - dimensionGroups, - groupId, - getColumnOrder(modifiedLayer), - columnId - ); + if (target.layerId === source.layerId) { + const updatedColumnOrder = reorderByGroups( + dimensionGroups, + getColumnOrder(modifiedLayers[target.layerId]), + target.groupId, + target.columnId + ); + + const newLayer = { + ...modifiedLayers[target.layerId], + columnOrder: updatedColumnOrder, + columns: modifiedLayers[target.layerId].columns, + }; + + // Time to replace + setState( + mergeLayer({ + state, + layerId: target.layerId, + newLayer, + }) + ); + return true; + } else { + setState(mergeLayers({ state, newLayers: modifiedLayers })); - // Time to replace - setState( - mergeLayer({ - state, - layerId, - newLayer: { - columnOrder: updatedColumnOrder, - columns: modifiedLayer.columns, - }, - }) - ); - return shouldDeleteSource ? { deleted: droppedItem.columnId } : true; + return true; + } } -function onReorder({ - columnId, - setState, - state, - layerId, - droppedItem, -}: DropHandlerProps) { - function reorderElements(items: string[], dest: string, src: string) { - const result = items.filter((c) => c !== src); - const targetIndex = items.findIndex((c) => c === src); - const sourceIndex = items.findIndex((c) => c === dest); - - const targetPosition = result.indexOf(dest); - result.splice(targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, 0, src); +function onReorder({ setState, state, source, target }: DropHandlerProps) { + function reorderElements(items: string[], targetId: string, sourceId: string) { + const result = items.filter((c) => c !== sourceId); + const targetIndex = items.findIndex((c) => c === sourceId); + const sourceIndex = items.findIndex((c) => c === targetId); + + const targetPosition = result.indexOf(targetId); + result.splice(targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, 0, sourceId); return result; } setState( mergeLayer({ state, - layerId, + layerId: target.layerId, newLayer: { columnOrder: reorderElements( - state.layers[layerId].columnOrder, - columnId, - droppedItem.columnId + state.layers[target.layerId].columnOrder, + target.columnId, + source.columnId ), }, }) @@ -272,124 +236,158 @@ function onReorder({ } function onMoveIncompatible( - { - columnId, - setState, - state, - layerId, - droppedItem, - filterOperations, - dimensionGroups, - groupId, - }: DropHandlerProps, + { setState, state, source, dimensionGroups, target }: DropHandlerProps, shouldDeleteSource?: boolean ) { - const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; - const sourceColumn = layer.columns[droppedItem.columnId]; - const targetColumn = layer.columns[columnId] || null; - + const targetLayer = state.layers[target.layerId]; + const targetColumn = targetLayer.columns[target.columnId] || null; + const sourceLayer = state.layers[source.layerId]; + const indexPattern = state.indexPatterns[sourceLayer.indexPatternId]; + const sourceColumn = sourceLayer.columns[source.columnId]; const sourceField = getField(sourceColumn, indexPattern); - const newOperation = getNewOperation(sourceField, filterOperations, targetColumn); + const newOperation = getNewOperation(sourceField, target.filterOperations, targetColumn); if (!newOperation) { return false; } - const modifiedLayer = shouldDeleteSource + const outputSourceLayer = shouldDeleteSource ? deleteColumn({ - layer, - columnId: droppedItem.columnId, + layer: sourceLayer, + columnId: source.columnId, indexPattern, }) - : layer; + : sourceLayer; - const newLayer = insertOrReplaceColumn({ - layer: modifiedLayer, - columnId, - indexPattern, - op: newOperation, - field: sourceField, - visualizationGroups: dimensionGroups, - targetGroup: groupId, - shouldResetLabel: true, - }); + if (target.layerId === source.layerId) { + const newLayer = insertOrReplaceColumn({ + layer: outputSourceLayer, + columnId: target.columnId, + indexPattern, + op: newOperation, + field: sourceField, + visualizationGroups: dimensionGroups, + targetGroup: target.groupId, + shouldResetLabel: true, + }); - trackUiEvent('drop_onto_dimension'); - setState( - mergeLayer({ - state, - layerId, - newLayer, - }) - ); - return shouldDeleteSource ? { deleted: droppedItem.columnId } : true; + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayer({ + state, + layerId: target.layerId, + newLayer, + }) + ); + return true; + } else { + const outputTargetLayer = insertOrReplaceColumn({ + layer: targetLayer, + columnId: target.columnId, + indexPattern, + op: newOperation, + field: sourceField, + visualizationGroups: dimensionGroups, + targetGroup: target.groupId, + shouldResetLabel: true, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayers({ + state, + newLayers: { + [source.layerId]: outputSourceLayer, + [target.layerId]: outputTargetLayer, + }, + }) + ); + return true; + } } function onSwapIncompatible({ - columnId, setState, state, - layerId, - droppedItem, - filterOperations, + source, dimensionGroups, - groupId, -}: DropHandlerProps) { - const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; - const sourceColumn = layer.columns[droppedItem.columnId]; - const targetColumn = layer.columns[columnId]; + target, +}: DropHandlerProps) { + const targetLayer = state.layers[target.layerId]; + const sourceLayer = state.layers[source.layerId]; + const indexPattern = state.indexPatterns[targetLayer.indexPatternId]; + const sourceColumn = sourceLayer.columns[source.columnId]; + const targetColumn = targetLayer.columns[target.columnId]; const sourceField = getField(sourceColumn, indexPattern); const targetField = getField(targetColumn, indexPattern); - const newOperationForSource = getNewOperation(sourceField, filterOperations, targetColumn); - const newOperationForTarget = getNewOperation( - targetField, - droppedItem.filterOperations, - sourceColumn - ); + const newOperationForSource = getNewOperation(sourceField, target.filterOperations, targetColumn); + const newOperationForTarget = getNewOperation(targetField, source.filterOperations, sourceColumn); if (!newOperationForSource || !newOperationForTarget) { return false; } - const newLayer = insertOrReplaceColumn({ - layer: insertOrReplaceColumn({ - layer, - columnId, - targetGroup: groupId, - indexPattern, - op: newOperationForSource, - field: sourceField, - visualizationGroups: dimensionGroups, - shouldResetLabel: true, - }), - columnId: droppedItem.columnId, + const outputTargetLayer = insertOrReplaceColumn({ + layer: targetLayer, + columnId: target.columnId, + targetGroup: target.groupId, indexPattern, - op: newOperationForTarget, - field: targetField, + op: newOperationForSource, + field: sourceField, visualizationGroups: dimensionGroups, - targetGroup: droppedItem.groupId, shouldResetLabel: true, }); - trackUiEvent('drop_onto_dimension'); - setState( - mergeLayer({ - state, - layerId, - newLayer, - }) - ); - return true; + if (source.layerId === target.layerId) { + const newLayer = insertOrReplaceColumn({ + layer: outputTargetLayer, + columnId: source.columnId, + indexPattern, + op: newOperationForTarget, + field: targetField, + visualizationGroups: dimensionGroups, + targetGroup: source.groupId, + shouldResetLabel: true, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayer({ + state, + layerId: target.layerId, + newLayer, + }) + ); + return true; + } else { + const outputSourceLayer = insertOrReplaceColumn({ + layer: sourceLayer, + columnId: source.columnId, + indexPattern, + op: newOperationForTarget, + field: targetField, + visualizationGroups: dimensionGroups, + targetGroup: source.groupId, + shouldResetLabel: true, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayers({ + state, + newLayers: { [source.layerId]: outputSourceLayer, [target.layerId]: outputTargetLayer }, + }) + ); + return true; + } } const swapColumnOrder = (columnOrder: string[], sourceId: string, targetId: string) => { - const newColumnOrder = [...columnOrder]; - const sourceIndex = newColumnOrder.findIndex((c) => c === sourceId); - const targetIndex = newColumnOrder.findIndex((c) => c === targetId); + const sourceIndex = columnOrder.findIndex((c) => c === sourceId); + const targetIndex = columnOrder.findIndex((c) => c === targetId); + const newColumnOrder = [...columnOrder]; newColumnOrder[sourceIndex] = targetId; newColumnOrder[targetIndex] = sourceId; @@ -397,38 +395,114 @@ const swapColumnOrder = (columnOrder: string[], sourceId: string, targetId: stri }; function onSwapCompatible({ - columnId, setState, state, - layerId, - droppedItem, + source, dimensionGroups, - groupId, -}: DropHandlerProps) { - const layer = state.layers[layerId]; - const sourceId = droppedItem.columnId; - const targetId = columnId; - - const sourceColumn = { ...layer.columns[sourceId] }; - const targetColumn = { ...layer.columns[targetId] }; - const newColumns = { ...layer.columns }; - newColumns[targetId] = sourceColumn; - newColumns[sourceId] = targetColumn; - - let updatedColumnOrder = swapColumnOrder(layer.columnOrder, sourceId, targetId); - updatedColumnOrder = reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); - - // Time to replace - setState( - mergeLayer({ - state, - layerId, - newLayer: { - columnOrder: updatedColumnOrder, - columns: newColumns, - }, - }) - ); + target, +}: DropHandlerProps) { + if (target.layerId === source.layerId) { + const layer = state.layers[target.layerId]; + const newColumns = { + ...layer.columns, + [target.columnId]: { ...layer.columns[source.columnId] }, + [source.columnId]: { ...layer.columns[target.columnId] }, + }; + + let updatedColumnOrder = swapColumnOrder(layer.columnOrder, source.columnId, target.columnId); + updatedColumnOrder = reorderByGroups( + dimensionGroups, + updatedColumnOrder, + target.groupId, + target.columnId + ); + + setState( + mergeLayer({ + state, + layerId: target.layerId, + newLayer: { + columnOrder: updatedColumnOrder, + columns: newColumns, + }, + }) + ); + + return true; + } else { + const newTargetLayer = copyColumn({ + layers: state.layers, + target, + source, + shouldDeleteSource: true, + })[target.layerId]; + + const newSourceLayer = copyColumn({ + layers: state.layers, + target: source, + source: target, + shouldDeleteSource: true, + })[source.layerId]; + + setState( + mergeLayers({ + state, + newLayers: { + [source.layerId]: newSourceLayer, + [target.layerId]: newTargetLayer, + }, + }) + ); + + return true; + } +} +function onCombine({ + state, + setState, + source, + target, + dimensionGroups, +}: DropHandlerProps) { + const targetLayer = state.layers[target.layerId]; + const targetColumn = targetLayer.columns[target.columnId]; + const targetField = getField(targetColumn, target.dataView); + const indexPattern = state.indexPatterns[targetLayer.indexPatternId]; + + const sourceLayer = state.layers[source.layerId]; + const sourceColumn = sourceLayer.columns[source.columnId]; + const sourceField = getField(sourceColumn, indexPattern); + // extract the field from the source column + if (!sourceField || !targetField) { + return false; + } + // pass it to the target column and delete the source column + const initialParams = { + params: + getOperationHelperForMultipleFields(targetColumn.operationType)?.({ + targetColumn, + sourceColumn, + indexPattern, + }) ?? {}, + }; + + const outputTargetLayer = replaceColumn({ + layer: targetLayer, + columnId: target.columnId, + indexPattern, + op: targetColumn.operationType, + field: targetField, + visualizationGroups: dimensionGroups, + targetGroup: target.groupId, + initialParams, + shouldCombineField: true, + }); + + const newLayers = deleteColumnInLayers({ + layers: { ...state.layers, [target.layerId]: outputTargetLayer }, + source, + }); + setState(mergeLayers({ state, newLayers })); return true; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 7a7297e77bcf2..2f703547219ec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -18,9 +18,9 @@ export interface OperationSupportMatrix { } type Props = Pick< - DatasourceDimensionDropProps, - 'layerId' | 'columnId' | 'state' | 'filterOperations' ->; + DatasourceDimensionDropProps['target'], + 'layerId' | 'columnId' | 'filterOperations' +> & { state: IndexPatternPrivateState }; function computeOperationMatrix( operationsByMetadata: Array<{ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx index 1d08873a160e9..72aace21479ac 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx @@ -174,13 +174,23 @@ export const formulaOperation: OperationDefinition { return true; }, - createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap) { - const currentColumn = layer.columns[sourceId] as FormulaIndexPatternColumn; - - return insertOrReplaceFormulaColumn(targetId, currentColumn, layer, { - indexPattern, - operations: operationDefinitionMap, - }).layer; + createCopy(layers, source, target, operationDefinitionMap) { + const currentColumn = layers[source.layerId].columns[ + source.columnId + ] as FormulaIndexPatternColumn; + const modifiedLayer = insertOrReplaceFormulaColumn( + target.columnId, + currentColumn, + layers[target.layerId], + { + indexPattern: target.dataView, + operations: operationDefinitionMap, + } + ); + return { + ...layers, + [target.layerId]: modifiedLayer.layer, + }; }, timeScalingMode: 'optional', paramEditor: WrappedFormulaEditor, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx index 85c2ea707b123..d7f25275f63a2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -67,8 +67,8 @@ export const mathOperation: OperationDefinition { - return { ...layer }; + createCopy: (layers) => { + return { ...layers }; }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 6ca79009ff95b..ad0dc9a1ef571 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -51,7 +51,12 @@ import type { GenericIndexPatternColumn, ReferenceBasedIndexPatternColumn, } from './column_types'; -import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types'; +import { + DataViewLensOperation, + IndexPattern, + IndexPatternField, + IndexPatternLayer, +} from '../../types'; import { DateRange, LayerType } from '../../../../common'; import { rangeOperation } from './ranges'; import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from '../../dimension_panel'; @@ -248,7 +253,7 @@ interface BaseOperationDefinitionProps onOtherColumnChanged?: ( layer: IndexPatternLayer, thisColumnId: string, - changedColumnId: string + changedColumnId?: string ) => C; /** * React component for operation specific settings shown in the flyout editor @@ -608,12 +613,11 @@ interface ManagedReferenceOperationDefinition * root level */ createCopy: ( - layer: IndexPatternLayer, - sourceColumnId: string, - targetColumnId: string, - indexPattern: IndexPattern, + layers: Record, + source: DataViewLensOperation, + target: DataViewLensOperation, operationDefinitionMap: Record - ) => IndexPatternLayer; + ) => Record; } interface OperationDefinitionMap { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index 555360a2f7f6c..5642c06c6b642 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -16,6 +16,7 @@ import { import type { IndexPattern } from '../../types'; import { useDebouncedValue } from '../../../shared_components'; import { getFormatFromPreviousColumn, isValidNumber } from './helpers'; +import { getColumnOrder } from '../layer_helpers'; const defaultLabel = i18n.translate('xpack.lens.indexPattern.staticValueLabelDefault', { defaultMessage: 'Static value', @@ -132,13 +133,21 @@ export const staticValueOperation: OperationDefinition< isTransferable: (column) => { return true; }, - createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap) { - const currentColumn = layer.columns[sourceId] as StaticValueIndexPatternColumn; + createCopy(layers, source, target) { + const currentColumn = layers[source.layerId].columns[ + source.columnId + ] as StaticValueIndexPatternColumn; + const targetLayer = layers[target.layerId]; + const columns = { + ...targetLayer.columns, + [target.columnId]: { ...currentColumn }, + }; return { - ...layer, - columns: { - ...layer.columns, - [targetId]: { ...currentColumn }, + ...layers, + [target.layerId]: { + ...targetLayer, + columns, + columnOrder: getColumnOrder({ ...targetLayer, columns }), }, }; }, 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 4fdd82439fc22..00a79b4d38a99 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 @@ -159,24 +159,38 @@ describe('state_helpers', () => { params: { window: 5 }, references: ['formulaX0'], }; + expect( copyColumn({ - layer: { - indexPatternId: '', - columnOrder: [], - columns: { - source, - formulaX0: sum, - formulaX1: movingAvg, - formulaX2: math, + layers: { + layer: { + indexPatternId: '', + columnOrder: [], + columns: { + source, + formulaX0: sum, + formulaX1: movingAvg, + formulaX2: math, + }, }, }, - targetId: 'copy', - sourceColumn: source, + source: { + column: source, + groupId: 'one', + columnId: 'source', + layerId: 'layer', + dataView: indexPattern, + filterOperations: () => true, + }, + target: { + columnId: 'copy', + groupId: 'one', + dataView: indexPattern, + layerId: 'layer', + filterOperations: () => true, + }, shouldDeleteSource: false, - indexPattern, - sourceColumnId: 'source', - }) + }).layer ).toEqual({ indexPatternId: '', columnOrder: [ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 8e8b56d7d862d..17c4d1e83a549 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -26,6 +26,7 @@ import { TermsIndexPatternColumn, } from './definitions'; import type { + DataViewLensOperation, IndexPattern, IndexPatternField, IndexPatternLayer, @@ -68,96 +69,84 @@ interface ColumnChange { } interface ColumnCopy { - layer: IndexPatternLayer; - targetId: string; - sourceColumn: GenericIndexPatternColumn; - sourceColumnId: string; - indexPattern: IndexPattern; + layers: Record; + target: DataViewLensOperation; + source: DataViewLensOperation; shouldDeleteSource?: boolean; } +export const deleteColumnInLayers = ({ + layers, + source, +}: { + layers: Record; + source: DataViewLensOperation; +}) => ({ + ...layers, + [source.layerId]: deleteColumn({ + layer: layers[source.layerId], + columnId: source.columnId, + indexPattern: source.dataView, + }), +}); + export function copyColumn({ - layer, - targetId, - sourceColumn, + layers, + source, + target, shouldDeleteSource, - indexPattern, - sourceColumnId, -}: ColumnCopy): IndexPatternLayer { - let modifiedLayer = copyReferencesRecursively( - layer, - sourceColumn, - sourceColumnId, - targetId, - indexPattern - ); - - if (shouldDeleteSource) { - modifiedLayer = deleteColumn({ - layer: modifiedLayer, - columnId: sourceColumnId, - indexPattern, - }); - } - - return modifiedLayer; +}: ColumnCopy): Record { + const outputLayers = createCopiedColumn(layers, target, source); + return shouldDeleteSource + ? deleteColumnInLayers({ + layers: outputLayers, + source, + }) + : outputLayers; } -function copyReferencesRecursively( - layer: IndexPatternLayer, - sourceColumn: GenericIndexPatternColumn, - sourceId: string, - targetId: string, - indexPattern: IndexPattern -): IndexPatternLayer { - let columns = { ...layer.columns }; +function createCopiedColumn( + layers: Record, + target: DataViewLensOperation, + source: DataViewLensOperation +): Record { + const sourceLayer = layers[source.layerId]; + const sourceColumn = sourceLayer.columns[source.columnId]; + const targetLayer = layers[target.layerId]; + let columns = { ...targetLayer.columns }; if ('references' in sourceColumn) { - if (columns[targetId]) { - return layer; - } - const def = operationDefinitionMap[sourceColumn.operationType]; if ('createCopy' in def) { - // Allow managed references to recursively insert new columns - return def.createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap); + return def.createCopy(layers, source, target, operationDefinitionMap); // Allow managed references to recursively insert new columns } + const referenceColumns = sourceColumn.references.reduce((refs, sourceRef) => { + const newRefId = generateId(); + return { ...refs, [newRefId]: { ...sourceLayer.columns[sourceRef] } }; + }, {}); - sourceColumn?.references.forEach((ref, index) => { - const newId = generateId(); - const refColumn = { ...columns[ref] }; - - // TODO: For fullReference types, now all references are hidden columns, - // but in the future we will have references to visible columns - // and visible columns shouldn't be copied - const refColumnWithInnerRefs = - 'references' in refColumn - ? copyReferencesRecursively(layer, refColumn, sourceId, newId, indexPattern).columns // if a column has references, copy them too - : { [newId]: refColumn }; - - const newColumn = columns[targetId]; - let references = [newId]; - if (newColumn && 'references' in newColumn) { - references = newColumn.references; - references[index] = newId; - } - - columns = { - ...columns, - ...refColumnWithInnerRefs, - [targetId]: { - ...sourceColumn, - references, - }, - }; - }); + columns = { + ...columns, + ...referenceColumns, + [target.columnId]: { + ...sourceColumn, + references: Object.keys(referenceColumns), + }, + }; } else { columns = { ...columns, - [targetId]: sourceColumn, + [target.columnId]: { ...sourceColumn }, }; } - return { ...layer, columns, columnOrder: getColumnOrder({ ...layer, columns }) }; + return { + ...layers, + [target.layerId]: adjustColumnReferences({ + ...targetLayer, + columns, + columnOrder: getColumnOrder({ ...targetLayer, columns }), + }), + }; } export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer { @@ -1046,8 +1035,8 @@ function addBucket( } updatedColumnOrder = reorderByGroups( visualizationGroups, - targetGroup, updatedColumnOrder, + targetGroup, addedColumnId ); const tempLayer = { @@ -1064,8 +1053,8 @@ function addBucket( export function reorderByGroups( visualizationGroups: VisualizationDimensionGroupConfig[], - targetGroup: string | undefined, updatedColumnOrder: string[], + targetGroup: string | undefined, addedColumnId: string ) { const hidesColumnGrouping = @@ -1184,6 +1173,26 @@ export function updateColumnParam({ }; } +export function adjustColumnReferences(layer: IndexPatternLayer) { + const newColumns = { ...layer.columns }; + Object.keys(newColumns).forEach((currentColumnId) => { + const currentColumn = newColumns[currentColumnId]; + if (currentColumn?.operationType) { + const operationDefinition = operationDefinitionMap[currentColumn.operationType]; + newColumns[currentColumnId] = operationDefinition.onOtherColumnChanged + ? operationDefinition.onOtherColumnChanged( + { ...layer, columns: newColumns }, + currentColumnId + ) + : currentColumn; + } + }); + return { + ...layer, + columns: newColumns, + }; +} + export function adjustColumnReferencesForChangedColumn( layer: IndexPatternLayer, changedColumnId: string @@ -1562,7 +1571,7 @@ export function isColumnValidAsReference({ const operationType = column.operationType; const operationDefinition = operationDefinitionMap[operationType]; return ( - validation.input.includes(operationDefinition.input) && + validation.input.includes(operationDefinition?.input) && maybeValidateOperations({ column, validation, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts index a8e71c0fd86e5..2a0a724e11379 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts @@ -47,7 +47,7 @@ export function adjustTimeScaleLabelSuffix( export function adjustTimeScaleOnOtherColumnChange( layer: IndexPatternLayer, thisColumnId: string, - changedColumnId: string + changedColumnId?: string ): T { const columns = layer.columns; const column = columns[thisColumnId] as T; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts index 5d48922a66d8a..6e16ebe5e8d53 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts @@ -24,3 +24,19 @@ export function mergeLayer({ }, }; } + +export function mergeLayers({ + state, + newLayers, +}: { + state: IndexPatternPrivateState; + newLayers: Record; +}) { + return { + ...state, + layers: { + ...state.layers, + ...newLayers, + }, + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 7e25509c3b2dd..eafa87e511b64 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -10,6 +10,7 @@ import type { FieldSpec } from '@kbn/data-plugin/common'; import type { FieldFormatParams } from '@kbn/field-formats-plugin/common'; import type { DragDropIdentifier } from '../drag_drop/providers'; import type { IncompleteColumn, GenericIndexPatternColumn } from './operations'; +import { DragDropOperation } from '../types'; export type { GenericIndexPatternColumn, @@ -109,3 +110,8 @@ export interface IndexPatternRef { title: string; name?: string; } + +export interface DataViewLensOperation extends DragDropOperation { + dataView: IndexPattern; + column?: GenericIndexPatternColumn; +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 4c2f0785e7a3e..e3406b85549d9 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -199,11 +199,18 @@ interface ChartSettings { }; } -export type GetDropProps = DatasourceDimensionDropProps & { - groupId: string; - dragging: DragContextState['dragging']; - prioritizedOperation?: string; -}; +export interface GetDropProps { + state: T; + source?: DraggingIdentifier; + target: { + layerId: string; + groupId: string; + columnId: string; + filterOperations: (meta: OperationMetadata) => boolean; + prioritizedOperation?: string; + isNewColumn?: boolean; + }; +} /** * Interface for the datasource registry @@ -259,7 +266,7 @@ export interface Datasource { getDropProps: ( props: GetDropProps ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; - onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; + onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean | undefined; /** * The datasource is allowed to cancel a close event on the dimension editor, * mainly used for formulas @@ -454,16 +461,14 @@ export interface DatasourceLayerPanelProps { activeData?: Record; } -export interface DraggedOperation extends DraggingIdentifier { +export interface DragDropOperation { layerId: string; groupId: string; columnId: string; filterOperations: (operation: OperationMetadata) => boolean; } -export function isDraggedOperation( - operationCandidate: unknown -): operationCandidate is DraggedOperation { +export function isOperation(operationCandidate: unknown): operationCandidate is DragDropOperation { return ( typeof operationCandidate === 'object' && operationCandidate !== null && @@ -471,10 +476,8 @@ export function isDraggedOperation( ); } -export type DatasourceDimensionDropProps = SharedDimensionProps & { - layerId: string; - groupId: string; - columnId: string; +export interface DatasourceDimensionDropProps { + target: DragDropOperation; state: T; setState: StateSetter< T, @@ -484,10 +487,10 @@ export type DatasourceDimensionDropProps = SharedDimensionProps & { } >; dimensionGroups: VisualizationDimensionGroupConfig[]; -}; +} -export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { - droppedItem: unknown; +export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { + source?: DragDropIdentifier; dropType: DropType; }; @@ -851,7 +854,14 @@ export interface Visualization { * look at its internal state to determine which dimension is being affected. */ removeDimension: (props: VisualizationDimensionChangeProps) => T; - + onDrop?: (props: { + prevState: T; + target: DragDropOperation; + source: DragDropIdentifier; + frame: FramePublicAPI; + dropType: DropType; + group?: VisualizationDimensionGroupConfig; + }) => T; /** * Update the configuration for the visualization. This is used to update the state */ diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx index 35a40623b72aa..3bd9ec3b641e5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -118,6 +118,205 @@ export const getAnnotationsSupportedLayer = ( }; }; +const createCopyAnnotation = ( + newId: string, + timestamp: string, + source?: EventAnnotationConfig +): EventAnnotationConfig => { + if (!source) { + return { + label: defaultAnnotationLabel, + key: { + type: 'point_in_time', + timestamp, + }, + icon: 'triangle', + id: newId, + }; + } + return { + ...source, + id: newId, + }; +}; + +export const onAnnotationDrop: Visualization['onDrop'] = ({ + prevState, + frame, + source, + target, + 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) + ) { + return prevState; + } + const targetAnnotation = targetLayer.annotations.find(({ id }) => id === target.columnId); + const sourceAnnotation = sourceLayer.annotations.find(({ id }) => id === source.columnId); + switch (dropType) { + case 'reorder': + if (!targetAnnotation || !sourceAnnotation || source.layerId !== target.layerId) { + return prevState; + } + const newAnnotations = targetLayer.annotations.filter((c) => c.id !== sourceAnnotation.id); + const targetPosition = newAnnotations.findIndex((c) => c.id === targetAnnotation.id); + const targetIndex = targetLayer.annotations.indexOf(sourceAnnotation); + const sourceIndex = targetLayer.annotations.indexOf(targetAnnotation); + newAnnotations.splice( + targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, + 0, + sourceAnnotation + ); + return { + ...prevState, + layers: prevState.layers.map((l) => + l.layerId === target.layerId ? { ...targetLayer, annotations: newAnnotations } : l + ), + }; + case 'swap_compatible': + if (!targetAnnotation || !sourceAnnotation) { + return prevState; + } + return { + ...prevState, + layers: prevState.layers.map((l): XYLayerConfig => { + if (!isAnnotationsLayer(l) || !isAnnotationsLayer(targetLayer)) { + return l; + } + if (l.layerId === target.layerId) { + return { + ...targetLayer, + annotations: [ + ...targetLayer.annotations.map( + (a): EventAnnotationConfig => (a === targetAnnotation ? sourceAnnotation : a) + ), + ], + }; + } + if (l.layerId === source.layerId) { + return { + ...sourceLayer, + annotations: [ + ...sourceLayer.annotations.map( + (a): EventAnnotationConfig => (a === sourceAnnotation ? targetAnnotation : a) + ), + ], + }; + } + return l; + }), + }; + case 'replace_compatible': + if (!targetAnnotation || !sourceAnnotation) { + return prevState; + } + + return { + ...prevState, + layers: prevState.layers.map((l) => { + if (l.layerId === source.layerId) { + return { + ...sourceLayer, + annotations: sourceLayer.annotations.filter((a) => a !== sourceAnnotation), + }; + } + if (l.layerId === target.layerId) { + return { + ...targetLayer, + annotations: [ + ...targetLayer.annotations.map((a) => + a === targetAnnotation ? sourceAnnotation : a + ), + ], + }; + } + return l; + }), + }; + case 'duplicate_compatible': + if (targetAnnotation) { + return prevState; + } + return { + ...prevState, + layers: prevState.layers.map( + (l): XYLayerConfig => + l.layerId === target.layerId + ? { + ...targetLayer, + annotations: [ + ...targetLayer.annotations, + createCopyAnnotation( + target.columnId, + getStaticDate(getDataLayers(prevState.layers), frame), + sourceAnnotation + ), + ], + } + : l + ), + }; + case 'replace_duplicate_compatible': + if (!targetAnnotation) { + return prevState; + } + return { + ...prevState, + layers: prevState.layers.map((l) => { + if (l.layerId === target.layerId) { + return { + ...targetLayer, + annotations: [ + ...targetLayer.annotations.map((a) => + a === targetAnnotation + ? createCopyAnnotation( + target.columnId, + getStaticDate(getDataLayers(prevState.layers), frame), + sourceAnnotation + ) + : a + ), + ], + }; + } + return l; + }), + }; + case 'move_compatible': + if (targetAnnotation || !sourceAnnotation) { + return prevState; + } + + return { + ...prevState, + layers: prevState.layers.map((l): XYLayerConfig => { + if (l.layerId === source.layerId) { + return { + ...sourceLayer, + annotations: sourceLayer.annotations.filter((a) => a !== sourceAnnotation), + }; + } + if (l.layerId === target.layerId) { + return { + ...targetLayer, + annotations: [...targetLayer.annotations, sourceAnnotation], + }; + } + return l; + }), + }; + default: + return prevState; + } + return prevState; +}; + export const setAnnotationsDimension: Visualization['setDimension'] = ({ prevState, layerId, @@ -125,46 +324,30 @@ export const setAnnotationsDimension: Visualization['setDimension'] = ( previousColumn, frame, }) => { - const foundLayer = prevState.layers.find((l) => l.layerId === layerId); - if (!foundLayer || !isAnnotationsLayer(foundLayer)) { + const targetLayer = prevState.layers.find((l) => l.layerId === layerId); + if (!targetLayer || !isAnnotationsLayer(targetLayer)) { return prevState; } - const inputAnnotations = foundLayer.annotations as XYAnnotationLayerConfig['annotations']; - const currentConfig = inputAnnotations?.find(({ id }) => id === columnId); - const previousConfig = previousColumn - ? inputAnnotations?.find(({ id }) => id === previousColumn) + const sourceAnnotation = previousColumn + ? targetLayer.annotations?.find(({ id }) => id === previousColumn) : undefined; - let resultAnnotations = [...inputAnnotations] as XYAnnotationLayerConfig['annotations']; - - if (!currentConfig) { - resultAnnotations.push({ - label: defaultAnnotationLabel, - key: { - type: 'point_in_time', - timestamp: getStaticDate(getDataLayers(prevState.layers), frame), - }, - icon: 'triangle', - ...previousConfig, - id: columnId, - }); - } else if (currentConfig && previousConfig) { - // TODO: reordering should not live in setDimension, to be refactored - resultAnnotations = inputAnnotations.filter((c) => c.id !== previousConfig.id); - const targetPosition = resultAnnotations.findIndex((c) => c.id === currentConfig.id); - const targetIndex = inputAnnotations.indexOf(previousConfig); - const sourceIndex = inputAnnotations.indexOf(currentConfig); - resultAnnotations.splice( - targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, - 0, - previousConfig - ); - } - return { ...prevState, layers: prevState.layers.map((l) => - l.layerId === layerId ? { ...foundLayer, annotations: resultAnnotations } : l + l.layerId === layerId + ? { + ...targetLayer, + annotations: [ + ...targetLayer.annotations, + createCopyAnnotation( + columnId, + getStaticDate(getDataLayers(prevState.layers), frame), + sourceAnnotation + ), + ], + } + : l ), }; }; @@ -224,7 +407,6 @@ export const getAnnotationsConfiguration = ({ defaultMessage: 'Annotations require a time based chart to work. Add a date histogram.', }), required: false, - requiresPreviousColumnOnDuplicate: true, supportsMoreColumns: true, supportFieldFormat: false, enableDimensionEditor: true, diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index 3f0e8816cf6b1..ece9a6d28893e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -18,6 +18,7 @@ import { checkScaleOperation, getAxisName, getDataLayers, + getReferenceLayers, isNumericMetric, isReferenceLayer, } from './visualization_helpers'; @@ -342,7 +343,10 @@ export const setReferenceDimension: Visualization['setDimension'] = ({ newLayer.accessors = [...newLayer.accessors.filter((a) => a !== columnId), columnId]; const hasYConfig = newLayer.yConfig?.some(({ forAccessor }) => forAccessor === columnId); const previousYConfig = previousColumn - ? newLayer.yConfig?.find(({ forAccessor }) => forAccessor === previousColumn) + ? getReferenceLayers(prevState.layers) + .map(({ yConfig }) => yConfig) + .flat() + ?.find((yConfig) => yConfig?.forAccessor === previousColumn) : false; if (!hasYConfig) { const axisMode: YAxisMode = diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index e1c63e4bb54d9..249b8f06a82c7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -474,7 +474,7 @@ describe('xy_visualization', () => { }); it('should copy previous column if passed and assign a new id', () => { expect( - xyVisualization.setDimension({ + xyVisualization.onDrop?.({ frame, prevState: { ...exampleState(), @@ -486,10 +486,20 @@ describe('xy_visualization', () => { }, ], }, - layerId: 'annotation', - groupId: 'xAnnotation', - previousColumn: 'an2', - columnId: 'newColId', + 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', @@ -499,7 +509,7 @@ describe('xy_visualization', () => { }); it('should reorder a dimension to a annotation layer', () => { expect( - xyVisualization.setDimension({ + xyVisualization.onDrop?.({ frame, prevState: { ...exampleState(), @@ -511,10 +521,21 @@ describe('xy_visualization', () => { }, ], }, - layerId: 'annotation', - groupId: 'xAnnotation', - previousColumn: 'an2', - columnId: 'an1', + 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', @@ -522,6 +543,199 @@ describe('xy_visualization', () => { annotations: [exampleAnnotation2, exampleAnnotation], }); }); + + it('should duplicate the annotations and replace the target in another annotation layer', () => { + expect( + xyVisualization.onDrop?.({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: 'annotations', + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: 'annotations', + annotations: [exampleAnnotation2], + }, + ], + }, + 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', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [{ ...exampleAnnotation, id: 'an2' }], + }, + ]); + }); + it('should swap the annotations between layers', () => { + expect( + xyVisualization.onDrop?.({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: 'annotations', + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: 'annotations', + annotations: [exampleAnnotation2], + }, + ], + }, + 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', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation2], + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ]); + }); + it('should replace the target in another annotation layer', () => { + expect( + xyVisualization.onDrop?.({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: 'annotations', + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: 'annotations', + annotations: [exampleAnnotation2], + }, + ], + }, + 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_compatible', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + annotations: [], + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ]); + }); + it('should move compatible to another annotation layer', () => { + expect( + xyVisualization.onDrop?.({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: 'annotations', + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: 'annotations', + annotations: [], + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + }, + dropType: 'move_compatible', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + annotations: [], + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ]); + }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 908a97d5c1c2d..532380d8549b7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -26,7 +26,7 @@ import { getSuggestions } from './xy_suggestions'; import { XyToolbar } from './xy_config_panel'; import { DimensionEditor } from './xy_config_panel/dimension_editor'; import { LayerHeader } from './xy_config_panel/layer_header'; -import type { Visualization, AccessorConfig, FramePublicAPI } from '../types'; +import { Visualization, AccessorConfig, FramePublicAPI } from '../types'; import { State, visualizationTypes, XYSuggestion, XYLayerConfig, XYDataLayerConfig } from './types'; import { layerTypes } from '../../common'; import { isHorizontalChart } from './state_helpers'; @@ -44,6 +44,7 @@ import { getAnnotationsSupportedLayer, setAnnotationsDimension, getUniqueLabels, + onAnnotationDrop, } from './annotations/helpers'; import { checkXAccessorCompatibility, @@ -70,6 +71,7 @@ import { ReferenceLinePanel } from './xy_config_panel/reference_line_config_pane import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel'; import { DimensionTrigger } from '../shared_components/dimension_trigger'; import { defaultAnnotationLabel } from './annotations/helpers'; +import { onDropForVisualization } from '../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; export const getXyVisualization = ({ paletteService, @@ -300,6 +302,20 @@ export const getXyVisualization = ({ return getFirstDataLayer(state.layers)?.palette; }, + onDrop(props) { + const targetLayer: XYLayerConfig | undefined = props.prevState.layers.find( + (l) => l.layerId === props.target.layerId + ); + if (!targetLayer) { + return props.prevState; + } + + if (isAnnotationsLayer(targetLayer)) { + return onAnnotationDrop?.(props) || props.prevState; + } + return onDropForVisualization(props, this); + }, + setDimension(props) { const { prevState, layerId, columnId, groupId } = props; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx index d390d081258a5..5a37169e7e196 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -332,8 +332,9 @@ export function validateLayersForDimension( }; } -export const isNumericMetric = (op: OperationMetadata) => - !op.isBucketed && op.dataType === 'number'; +export const isNumericMetric = (op: OperationMetadata) => { + return !op.isBucketed && op.dataType === 'number'; +}; export const isNumericDynamicMetric = (op: OperationMetadata) => isNumericMetric(op) && !op.isStaticValue; export const isBucketed = (op: OperationMetadata) => op.isBucketed; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4a1b4c39f5548..f58c84eead3f2 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -209,48 +209,14 @@ "xpack.lens.dragDrop.announce.cancelled": "Mouvement annulé. {label} revenu à sa position initiale", "xpack.lens.dragDrop.announce.cancelledItem": "Mouvement annulé. {label} revenu au groupe {groupLabel} à la position {position}", "xpack.lens.dragDrop.announce.combine.short": " Maintenir la touche Contrôle enfoncée pour combiner", - "xpack.lens.dragDrop.announce.dropped.combineCompatible": "Combinaison de {label} avec {dropGroupLabel} à la position {dropPosition} et de {dropLabel} avec {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.combineIncompatible": "Conversion de {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} et combinaison avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}", - "xpack.lens.dragDrop.announce.dropped.duplicated": "{label} dupliqué dans le groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.duplicateIncompatible": "Copie de {label} convertie en {nextLabel} et ajoutée au groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.moveCompatible": "{label} déplacé dans le groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.moveIncompatible": "{label} converti en {nextLabel} et déplacé dans le groupe {groupLabel} à la position {position}", "xpack.lens.dragDrop.announce.dropped.reordered": "{label} réorganisé dans le groupe {groupLabel} de la position {prevPosition} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible": "Copie de {label} convertie en {nextLabel} et {dropLabel} remplacé dans le groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.replaceIncompatible": "{label} converti en {nextLabel} et {dropLabel} remplacé dans le groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.swapCompatible": "{label} déplacé dans {dropGroupLabel} à la position {dropPosition} et {dropLabel} dans {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.swapIncompatible": "{label} converti en {nextLabel} dans le groupe {groupLabel} à la position {position} et permuté avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}", - "xpack.lens.dragDrop.announce.droppedDefault": "{label} ajouté dans le groupe {dropGroupLabel} à la position {position}", "xpack.lens.dragDrop.announce.droppedNoPosition": "{label} ajouté à {dropLabel}", "xpack.lens.dragDrop.announce.duplicate.short": " Maintenez la touche Alt ou Option enfoncée pour dupliquer.", - "xpack.lens.dragDrop.announce.duplicated.combine": "Combiner {dropLabel} avec {label} dans {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.duplicated.replace": "{dropLabel} remplacé par {label} dans {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible": "{dropLabel} remplacé par une copie de {label} dans {groupLabel} à la position {position}", "xpack.lens.dragDrop.announce.lifted": "{label} levé", - "xpack.lens.dragDrop.announce.selectedTarget.combine": "Combinez {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} avec {label}. Appuyez sur la barre d’espace ou sur Entrée pour combiner.", - "xpack.lens.dragDrop.announce.selectedTarget.combineCompatible": "Combinez {label} dans le groupe {groupLabel} à la position {position} avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenez la touche Contrôle enfoncée et appuyez sur la barre d’espace ou sur Entrée pour combiner.", - "xpack.lens.dragDrop.announce.selectedTarget.combineIncompatible": "Convertissez {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} et combinez avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenez la touche Contrôle enfoncée et appuyez sur la barre d’espace ou sur Entrée pour combiner.", - "xpack.lens.dragDrop.announce.selectedTarget.combineMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d’espace ou sur Entrée pour combiner {dropLabel} avec {label}.{duplicateCopy}{swapCopy}{combineCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.default": "Ajoutez {label} au groupe {dropGroupLabel} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour ajouter", "xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition": "Ajoutez {label} à {dropLabel}. Appuyer sur la barre d'espace ou sur Entrée pour ajouter", - "xpack.lens.dragDrop.announce.selectedTarget.duplicated": "Dupliquez {label} dans le groupe {dropGroupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer", - "xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup": "Dupliquez {label} dans le groupe {dropGroupLabel} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour dupliquer", - "xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible": "Convertissez la copie de {label} en {nextLabel} et ajoutez-la au groupe {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatible": "Déplacez {label} dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour déplacer", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain": "Vous faites glisser {label} de {groupLabel} à la position {position} vers la position {dropPosition} dans le groupe {dropGroupLabel}. Appuyez sur la barre d'espace ou sur Entrée pour déplacer.{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible": "Convertissez {label} en {nextLabel} et déplacez-le dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour déplacer", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain": "Vous faites glisser {label} de {groupLabel} à la position {position} vers la position {dropPosition} dans le groupe {dropGroupLabel}. Appuyez sur la barre d'espace ou sur Entrée pour convertir {label} en {nextLabel} et déplacer.{duplicateCopy}{swapCopy}", "xpack.lens.dragDrop.announce.selectedTarget.noSelected": "Aucune cible sélectionnée. Utiliser les touches fléchées pour sélectionner une cible", "xpack.lens.dragDrop.announce.selectedTarget.reordered": "Réorganisez {label} dans le groupe {groupLabel} de la position {prevPosition} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour réorganiser", "xpack.lens.dragDrop.announce.selectedTarget.reorderedBack": "{label} revenu à sa position initiale {prevPosition}", - "xpack.lens.dragDrop.announce.selectedTarget.replace": "Remplacez {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} avec {label}. Appuyez sur la barre d'espace ou sur Entrée pour remplacer.", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible": "Dupliquez {label} et remplacez {dropLabel} dans {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer et remplacer", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible": "Convertissez la copie de {label} en {nextLabel} et remplacez {dropLabel} dans le groupe {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer et remplacer", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible": "Convertissez {label} en {nextLabel} et remplacez {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour remplacer", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour convertir {label} en {nextLabel} et remplacer {dropLabel}.{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.replaceMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour remplacer {dropLabel} par {label}.{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.swapCompatible": "Permutez {label} dans le groupe {groupLabel} à la position {position} avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenir la touche Maj enfoncée tout en appuyant sur la barre d'espace ou sur Entrée pour permuter", - "xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible": "Convertir {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} et permutez avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenir la touche Maj enfoncée tout en appuyant sur la barre d'espace ou sur Entrée pour permuter", "xpack.lens.dragDrop.announce.swap.short": " Maintenez la touche Maj enfoncée pour permuter.", "xpack.lens.dragDrop.combine": "Combiner", "xpack.lens.dragDrop.control": "Contrôle", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5e3448cfa19c6..10f90af87773f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -214,43 +214,14 @@ "xpack.lens.dragDrop.announce.cancelled": "移動がキャンセルされました。{label}は初期位置に戻りました", "xpack.lens.dragDrop.announce.cancelledItem": "移動がキャンセルされました。{label}は位置{position}の{groupLabel}グループに戻りました", "xpack.lens.dragDrop.announce.combine.short": " Ctrlを押しながら結合します", - "xpack.lens.dragDrop.announce.dropped.combineCompatible": "位置{dropPosition}で{label}を{dropGroupLabel}に移動し、位置{position}で{dropLabel}を {groupLabel}グループに結合しました", - "xpack.lens.dragDrop.announce.dropped.combineIncompatible": "位置{position}で{label}を{groupLabel}の{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループの{dropLabel}と結合しました", - "xpack.lens.dragDrop.announce.dropped.duplicated": "位置{position}の{groupLabel}グループで{label}を複製しました", - "xpack.lens.dragDrop.announce.dropped.duplicateIncompatible": "{label}のコピーを{nextLabel}に変換し、位置{position}の{groupLabel}グループに追加しました", - "xpack.lens.dragDrop.announce.dropped.moveCompatible": "位置{position}の{groupLabel}グループに{label}を移動しました", - "xpack.lens.dragDrop.announce.dropped.moveIncompatible": "{label}を{nextLabel}に変換し、位置{position}の{groupLabel}グループに移動しました", "xpack.lens.dragDrop.announce.dropped.reordered": "{groupLabel}グループの{label}を位置{prevPosition}から位置{position}に並べ替えました", - "xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible": "{label}のコピーを{nextLabel}に変換し、位置{position}の{groupLabel}グループで{dropLabel}を置き換えました", - "xpack.lens.dragDrop.announce.dropped.replaceIncompatible": "{label}を{nextLabel}に変換し、位置{position}の{groupLabel}グループで{dropLabel}を置き換えました", - "xpack.lens.dragDrop.announce.dropped.swapCompatible": "位置{dropPosition}で{label}を{dropGroupLabel}に移動し、位置{position}で{dropLabel}を {groupLabel}グループに移動しました", - "xpack.lens.dragDrop.announce.dropped.swapIncompatible": "位置{position}で{label}を{groupLabel}の{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループの{dropLabel}と入れ替えました", - "xpack.lens.dragDrop.announce.droppedDefault": "位置{position}の{dropGroupLabel}グループで{label}を追加しました", "xpack.lens.dragDrop.announce.droppedNoPosition": "{label}を{dropLabel}に追加しました", "xpack.lens.dragDrop.announce.duplicate.short": " AltキーまたはOptionを押し続けると複製します。", - "xpack.lens.dragDrop.announce.duplicated.combine": "位置{position}の{groupLabel}で{dropLabel}を{label}と結合しました", - "xpack.lens.dragDrop.announce.duplicated.replace": "位置{position}の{groupLabel}で{dropLabel}を{label}に置き換えました", - "xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible": "位置{position}の{groupLabel}で{dropLabel}を{label}のコピーに置き換えました", "xpack.lens.dragDrop.announce.lifted": "{label}を上げました", - "xpack.lens.dragDrop.announce.selectedTarget.combineCompatible": "位置{position}の{groupLabel}の{label}を、位置{dropPosition}の{dropGroupLabel}グループの{dropLabel}と結合します。Ctrlキーを押しながらスペースバーまたはEnterキーを押すと、結合します", - "xpack.lens.dragDrop.announce.selectedTarget.combineIncompatible": "位置{position}で{label}を{groupLabel}の{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループの{dropLabel}と結合します。Ctrlキーを押しながらスペースバーまたはEnterキーを押すと、結合します", - "xpack.lens.dragDrop.announce.selectedTarget.default": "位置{position}の{dropGroupLabel}グループに{label}を追加しました。スペースまたはEnterを押して追加します", "xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition": "{label}を{dropLabel}に追加します。スペースまたはEnterを押して追加します", - "xpack.lens.dragDrop.announce.selectedTarget.duplicated": "位置{position}の{dropGroupLabel}グループに{label}を複製しました。AltまたはOptionを押しながらスペースバーまたはEnterキーを押すと、複製します", - "xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup": "位置{position}の{dropGroupLabel}グループに{label}を複製しました。スペースまたはEnterを押して複製します", - "xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible": "{label}のコピーを{nextLabel}に変換し、位置{position}で{groupLabel}グループに移動します。AltまたはOptionを押しながらスペースバーまたはEnterキーを押すと、複製します", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain": "位置{position}で{groupLabel}の{label}を{dropGroupLabel}グループの位置{dropPosition}にドラッグしています。スペースバーまたはEnterキーを押すと移動します。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible": "{label}を{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループに移動します。スペースまたはEnterを押して移動します", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain": "位置{position}で{groupLabel}の{label}を{dropGroupLabel}グループの位置{dropPosition}にドラッグしています。スペースバーまたはEnterキーを押して、{label}を{nextLabel}に変換して移動します。{duplicateCopy}{swapCopy}", "xpack.lens.dragDrop.announce.selectedTarget.noSelected": "対象が選択されていません。矢印キーを使用して対象を選択してください", "xpack.lens.dragDrop.announce.selectedTarget.reordered": "{groupLabel}グループの{label}を位置{prevPosition}から位置{position}に並べ替えます。スペースまたはEnterを押して並べ替えます", "xpack.lens.dragDrop.announce.selectedTarget.reorderedBack": "{label}は初期位置{prevPosition}に戻りました", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible": "位置{position}で{label}を複製し、{groupLabel}グループで{dropLabel}を置き換えます。AltまたはOptionを押しながらスペースバーまたはEnterキーを押すと、複製して置換します", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible": "{label}のコピーを{nextLabel}に変換し、位置{position}で{groupLabel}グループの{dropLabel}を置き換えます。AltまたはOptionを押しながらスペースバーまたはEnterキーを押すと、複製して置換します", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain": "位置{position}の{groupLabel}の{label}を、位置{dropPosition}の{dropGroupLabel}グループの{dropLabel}にドラッグしています。スペースバーまたはEnterキーを押して、{label}を{nextLabel}に変換して、{dropLabel}を置き換えます。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.replaceMain": "位置{position}の{groupLabel}の{label}を、位置{dropPosition}の{dropGroupLabel}グループの{dropLabel}にドラッグしています。スペースまたはEnterを押して、{dropLabel}を{label}で置き換えます。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.swapCompatible": "位置{position}の{groupLabel}の{label}を、位置{dropPosition}の{dropGroupLabel}グループの{dropLabel}と入れ替えます。Shiftキーを押しながらスペースバーまたはEnterキーを押すと、入れ替えます", - "xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible": "位置{position}で{label}を{groupLabel}の{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループの{dropLabel}と入れ替えます。Shiftキーを押しながらスペースバーまたはEnterキーを押すと、入れ替えます", "xpack.lens.dragDrop.announce.swap.short": " Shiftキーを押すと入れ替えます。", "xpack.lens.dragDrop.combine": "結合", "xpack.lens.dragDrop.control": "Control", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 465a3d78860af..a226a816ff04f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -214,48 +214,14 @@ "xpack.lens.dragDrop.announce.cancelled": "移动已取消。{label} 将返回至其初始位置", "xpack.lens.dragDrop.announce.cancelledItem": "移动已取消。{label} 返回至 {groupLabel} 组中的位置 {position}", "xpack.lens.dragDrop.announce.combine.short": " 按住 Control 键组合", - "xpack.lens.dragDrop.announce.dropped.combineCompatible": "已将 {label} 组合到 {dropGroupLabel} 中的位置 {dropPosition} 并将 {dropLabel} 组合到组 {groupLabel} 中的位置 {position}", - "xpack.lens.dragDrop.announce.dropped.combineIncompatible": "已将 {label} 转换为组 {groupLabel} 中位置 {position} 上的 {nextLabel},并与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 组合", - "xpack.lens.dragDrop.announce.dropped.duplicated": "已在 {groupLabel} 组中的位置 {position} 复制 {label}", - "xpack.lens.dragDrop.announce.dropped.duplicateIncompatible": "已将 {label} 的副本转换为 {nextLabel} 并添加 {groupLabel} 组中的位置 {position}", - "xpack.lens.dragDrop.announce.dropped.moveCompatible": "已将 {label} 移到 {groupLabel} 组中的位置 {position}", - "xpack.lens.dragDrop.announce.dropped.moveIncompatible": "已将 {label} 转换为 {nextLabel} 并移到 {groupLabel} 组中的位置 {position}", "xpack.lens.dragDrop.announce.dropped.reordered": "已将 {groupLabel} 组中的 {label} 从位置 {prevPosition} 重新排到位置 {position}", - "xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible": "已将 {label} 的副本转换为 {nextLabel} 并替换了 {groupLabel} 组中位置 {position} 上的 {dropLabel}", - "xpack.lens.dragDrop.announce.dropped.replaceIncompatible": "已将 {label} 转换为 {nextLabel} 并替换了 {groupLabel} 组中位置 {position} 上的 {dropLabel}", - "xpack.lens.dragDrop.announce.dropped.swapCompatible": "已将 {label} 移至 {dropGroupLabel} 中的位置 {dropPosition} 并将 {dropLabel} 移至组 {groupLabel} 中的位置 {position}", - "xpack.lens.dragDrop.announce.dropped.swapIncompatible": "已将 {label} 转换为组 {groupLabel} 中位置 {position} 上的 {nextLabel},并与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 交换", - "xpack.lens.dragDrop.announce.droppedDefault": "已将 {label} 添加到 {dropGroupLabel} 组中的位置 {position}", "xpack.lens.dragDrop.announce.droppedNoPosition": "已将 {label} 添加到 {dropLabel}", "xpack.lens.dragDrop.announce.duplicate.short": " 按住 alt 或 option 键以复制。", - "xpack.lens.dragDrop.announce.duplicated.combine": "将 {dropLabel} 与 {groupLabel} 中位置 {position} 上的 {label} 组合", - "xpack.lens.dragDrop.announce.duplicated.replace": "已将 {groupLabel} 组中位置 {position} 上的 {dropLabel} 替换为 {label}", - "xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible": "已将 {groupLabel} 组中位置 {position} 上的 {dropLabel} 替换为 {label} 的副本", "xpack.lens.dragDrop.announce.lifted": "已提升 {label}", - "xpack.lens.dragDrop.announce.selectedTarget.combine": "将 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel} 与 {label} 组合。按空格键或 enter 键组合。", - "xpack.lens.dragDrop.announce.selectedTarget.combineCompatible": "将组 {groupLabel} 中位置 {position} 上的 {label} 与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 组合。按住 Control 键并按空格键或 enter 键组合", - "xpack.lens.dragDrop.announce.selectedTarget.combineIncompatible": "将 {label} 转换为组 {groupLabel} 中位置 {position} 上的 {nextLabel},并与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 组合。按住 Control 键并按空格键或 enter 键组合", - "xpack.lens.dragDrop.announce.selectedTarget.combineMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel}。按空格键或 enter 键以将 {dropLabel} 与 {label} 组合。{duplicateCopy}{swapCopy}{combineCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.default": "将 {label} 添加到 {dropGroupLabel} 组中的位置 {position}。按空格键或 enter 键添加", "xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition": "将 {label} 添加到 {dropLabel}。按空格键或 enter 键添加", - "xpack.lens.dragDrop.announce.selectedTarget.duplicated": "将 {label} 复制到 {dropGroupLabel} 组中的位置 {position}。按住 Alt 或 Option 并按空格键或 enter 键以复制", - "xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup": "将 {label} 复制到 {dropGroupLabel} 组中的位置 {position}。按空格键或 enter 键复制", - "xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible": "将 {label} 转换为 {nextLabel} 并移到 {groupLabel} 组中的位置 {position}。按住 Alt 或 Option 并按空格键或 enter 键以复制", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatible": "将 {label} 移至 {dropGroupLabel} 组中的位置 {dropPosition}。按空格键或 enter 键移动", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中的位置 {dropPosition} 上。按空格键或 enter 键移动。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible": "将 {label} 转换为 {nextLabel} 并移到 {dropGroupLabel} 组中的位置 {dropPosition}。按空格键或 enter 键移动", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中的位置 {dropPosition} 上。按空格键或 enter 键以将 {label} 转换为 {nextLabel} 并移动。{duplicateCopy}{swapCopy}", "xpack.lens.dragDrop.announce.selectedTarget.noSelected": "未选择任何目标。使用箭头键选择目标", "xpack.lens.dragDrop.announce.selectedTarget.reordered": "将 {groupLabel} 组中的 {label} 从位置 {prevPosition} 重新排到位置 {position}。按空格键或 enter 键重新排列", "xpack.lens.dragDrop.announce.selectedTarget.reorderedBack": "{label} 已返回至其初始位置 {prevPosition}", - "xpack.lens.dragDrop.announce.selectedTarget.replace": "将 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel} 替换为 {label}。按空格键或 enter 键替换。", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible": "复制 {label} 并替换 {groupLabel} 中位置 {position} 上的 {dropLabel}。按住 Alt 或 Option 并按空格键或 enter 键以复制并替换", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible": "将 {label} 的副本转换为 {nextLabel} 并替换 {groupLabel} 组中位置 {position} 上的 {dropLabel}。按住 Alt 或 Option 并按空格键或 enter 键以复制并替换", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible": "将 {label} 转换为 {nextLabel} 并替换 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel}。按空格键或 enter 键替换", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel}。按空格键或 enter 键以将 {label} 转换为 {nextLabel} 并替换 {dropLabel}。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.replaceMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel}。按空格键或 enter 键以将 {dropLabel} 替换为 {label}。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.swapCompatible": "将组 {groupLabel} 中位置 {position} 上的 {label} 与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 交换。按住 Shift 键并按空格键或 enter 键交换", - "xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible": "将 {label} 转换为组 {groupLabel} 中位置 {position} 上的 {nextLabel},并与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 交换。按住 Shift 键并按空格键或 enter 键交换", "xpack.lens.dragDrop.announce.swap.short": " 按住 Shift 键交换。", "xpack.lens.dragDrop.combine": "组合", "xpack.lens.dragDrop.control": "Control 键", diff --git a/x-pack/test/functional/apps/lens/group1/table.ts b/x-pack/test/functional/apps/lens/group1/table.ts index 18ecc2e90cfe4..7bf9b49c53d8d 100644 --- a/x-pack/test/functional/apps/lens/group1/table.ts +++ b/x-pack/test/functional/apps/lens/group1/table.ts @@ -73,10 +73,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should allow to transpose columns', async () => { - await PageObjects.lens.dragDimensionToDimension( - 'lnsDatatable_rows > lns-dimensionTrigger', - 'lnsDatatable_columns > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsDatatable_rows > lns-dimensionTrigger', + to: 'lnsDatatable_columns > lns-empty-dimension', + }); expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('@timestamp per 3 hours'); expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal( '169.228.188.120 › Average of bytes' diff --git a/x-pack/test/functional/apps/lens/group3/annotations.ts b/x-pack/test/functional/apps/lens/group3/annotations.ts index 2b641c6c161d4..62e5edb564871 100644 --- a/x-pack/test/functional/apps/lens/group3/annotations.ts +++ b/x-pack/test/functional/apps/lens/group3/annotations.ts @@ -51,10 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should duplicate the style when duplicating an annotation and group them in the chart', async () => { // drag and drop to the empty field to generate a duplicate - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_xAnnotationsPanel > lns-dimensionTrigger', - 'lnsXY_xAnnotationsPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_xAnnotationsPanel > lns-dimensionTrigger', + to: 'lnsXY_xAnnotationsPanel > lns-empty-dimension', + }); await ( await find.byCssSelector( diff --git a/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts b/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts index dec72008d6f04..e56addf24396c 100644 --- a/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts @@ -8,8 +8,10 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ getPageObjects }: FtrProviderContext) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + + const listingTable = getService('listingTable'); const xyChartContainer = 'xyVisChart'; describe('lens drag and drop tests', () => { @@ -72,10 +74,10 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql(['Top 3 values of clientip']); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_xDimensionPanel > lns-dimensionTrigger', - 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lns-layerPanel-0 > lnsXY_xDimensionPanel > lns-dimensionTrigger', + to: 'lns-layerPanel-0 > lnsXY_splitDimensionPanel > lns-dimensionTrigger', + }); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql( [] @@ -90,10 +92,10 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql(['Top 3 values of @message.raw']); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', - 'lnsXY_yDimensionPanel > lns-dimensionTrigger' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + }); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') @@ -106,14 +108,14 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); }); it('should duplicate the column when dragging to empty dimension in the same group', async () => { - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yDimensionPanel > lns-dimensionTrigger', - 'lnsXY_yDimensionPanel > lns-empty-dimension' - ); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yDimensionPanel > lns-dimensionTrigger', - 'lnsXY_yDimensionPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_yDimensionPanel > lns-empty-dimension', + }); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_yDimensionPanel > lns-empty-dimension', + }); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ 'Unique count of @message.raw', 'Unique count of @message.raw [1]', @@ -121,10 +123,10 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); }); it('should move duplicated column to non-compatible dimension group', async () => { - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yDimensionPanel > lns-dimensionTrigger', - 'lnsXY_xDimensionPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_xDimensionPanel > lns-empty-dimension', + }); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ 'Unique count of @message.raw', 'Unique count of @message.raw [1]', @@ -340,5 +342,127 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); }); }); + + describe('dropping between layers', () => { + it('should move the column', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.createLayer('data'); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lns-layerPanel-0 > lnsXY_xDimensionPanel > lns-dimensionTrigger', + through: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension', + to: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lnsDragDropExtraDrops >lnsDragDrop', + }); + await PageObjects.lens.assertFocusedDimension('@timestamp [1]'); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + through: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension', + to: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lnsDragDropExtraDrops >lnsDragDrop', + }); + await PageObjects.lens.assertFocusedDimension('Average of bytes [1]'); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'Average of bytes', + 'Top values of ip', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + '@timestamp [1]', + 'Average of bytes [1]', + ]); + }); + + it('should move formula to empty dimension', async () => { + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'formula', + formula: `moving_average(average(bytes), window=5`, + }); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + through: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension', + to: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lnsDragDropExtraDrops >lnsDragDrop', + }); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'moving_average(average(bytes), window=5)', + 'Top 3 values of ip', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + '@timestamp [1]', + 'moving_average(average(bytes), window=5) [1]', + ]); + }); + + it('should replace formula with another formula', async () => { + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'formula', + formula: `sum(bytes) + 5`, + }); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + }); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'Top 3 values of ip', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + '@timestamp [1]', + 'moving_average(average(bytes), window=5)', + ]); + }); + it('swaps dimensions', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.createLayer('data'); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'bytes', + 'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-empty-dimension' + ); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'bytes', + 'lns-layerPanel-1 > lnsXY_splitDimensionPanel > lns-empty-dimension' + ); + + await PageObjects.lens.dragDimensionToDimension({ + from: `lns-layerPanel-1 > lnsXY_splitDimensionPanel > lns-dimensionTrigger`, + through: `lns-layerPanel-0 > lnsXY_splitDimensionPanel > lns-dimensionTrigger`, + to: `lns-layerPanel-0 > lnsXY_splitDimensionPanel > lnsDragDrop-swap`, + }); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'Average of bytes', + 'Median of bytes', + 'bytes', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + 'Top 3 values of ip', + ]); + }); + it('can combine dimensions', async () => { + await PageObjects.lens.dragDimensionToDimension({ + from: `lns-layerPanel-0 > lnsXY_splitDimensionPanel > lns-dimensionTrigger`, + through: `lns-layerPanel-1 > lnsXY_splitDimensionPanel > lns-dimensionTrigger`, + to: `lns-layerPanel-1 > lnsXY_splitDimensionPanel > lnsDragDrop-combine`, + }); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'Average of bytes', + 'Median of bytes', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + 'Top values of ip + 1 other', + ]); + }); + }); }); } diff --git a/x-pack/test/functional/apps/lens/group3/error_handling.ts b/x-pack/test/functional/apps/lens/group3/error_handling.ts index 8f6659bda1562..794547fb96f05 100644 --- a/x-pack/test/functional/apps/lens/group3/error_handling.ts +++ b/x-pack/test/functional/apps/lens/group3/error_handling.ts @@ -56,10 +56,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.waitForMissingDataViewWarning(); await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); await PageObjects.lens.closeDimensionEditor(); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yDimensionPanel > lns-dimensionTrigger', - 'lnsXY_yDimensionPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_yDimensionPanel > lns-empty-dimension', + }); await PageObjects.lens.switchFirstLayerIndexPattern('log*'); await PageObjects.lens.waitForMissingDataViewWarningDisappear(); await PageObjects.lens.waitForEmptyWorkspace(); diff --git a/x-pack/test/functional/apps/lens/group3/formula.ts b/x-pack/test/functional/apps/lens/group3/formula.ts index 33a24d3aefb1c..806e892cec643 100644 --- a/x-pack/test/functional/apps/lens/group3/formula.ts +++ b/x-pack/test/functional/apps/lens/group3/formula.ts @@ -205,10 +205,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); - await PageObjects.lens.dragDimensionToDimension( - 'lnsDatatable_metrics > lns-dimensionTrigger', - 'lnsDatatable_metrics > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsDatatable_metrics > lns-dimensionTrigger', + to: 'lnsDatatable_metrics > lns-empty-dimension', + }); expect(await PageObjects.lens.getDatatableCellText(1, 1)).to.eql('222,420'); expect(await PageObjects.lens.getDatatableCellText(1, 2)).to.eql('222,420'); }); @@ -249,15 +249,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.createLayer('referenceLine'); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', - operation: 'formula', - formula: `count()`, - keepOpen: true, - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', + operation: 'formula', + formula: `count()`, + keepOpen: true, + }); await PageObjects.lens.switchToStaticValue(); await PageObjects.lens.closeDimensionEditor(); @@ -280,10 +277,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { formula: `0`, }); - await PageObjects.lens.dragDimensionToDimension( - 'lnsDatatable_metrics > lns-dimensionTrigger', - 'lnsDatatable_metrics > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsDatatable_metrics > lns-dimensionTrigger', + to: 'lnsDatatable_metrics > lns-empty-dimension', + }); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('0'); expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('0'); }); diff --git a/x-pack/test/functional/apps/lens/group3/reference_lines.ts b/x-pack/test/functional/apps/lens/group3/reference_lines.ts index f022a6cef6e7a..ab2bc48ba57b8 100644 --- a/x-pack/test/functional/apps/lens/group3/reference_lines.ts +++ b/x-pack/test/functional/apps/lens/group3/reference_lines.ts @@ -86,10 +86,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); // drag and drop it to the left axis - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', - 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', + to: 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension', + }); await testSubjects.click('lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger'); expect( @@ -100,10 +100,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should duplicate also the original style when duplicating a reference line', async () => { // drag and drop to the empty field to generate a duplicate - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger', - 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger', + to: 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension', + }); await ( await find.byCssSelector( diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index d1125fbb08175..c57ec5f9452f6 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -111,22 +111,19 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param opts.field - the desired field for the dimension * @param layerIndex - the index of the layer */ - async configureDimension( - opts: { - dimension: string; - operation: string; - field?: string; - isPreviousIncompatible?: boolean; - keepOpen?: boolean; - palette?: string; - formula?: string; - disableEmptyRows?: boolean; - }, - layerIndex = 0 - ) { + async configureDimension(opts: { + dimension: string; + operation: string; + field?: string; + isPreviousIncompatible?: boolean; + keepOpen?: boolean; + palette?: string; + formula?: string; + disableEmptyRows?: boolean; + }) { await retry.try(async () => { if (!(await testSubjects.exists('lns-indexPattern-dimensionContainerClose'))) { - await testSubjects.click(`lns-layerPanel-${layerIndex} > ${opts.dimension}`); + await testSubjects.click(opts.dimension); } await testSubjects.existOrFail('lns-indexPattern-dimensionContainerClose'); }); @@ -450,11 +447,21 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param from - the selector of the dimension being moved * @param to - the selector of the dimension being dropped to * */ - async dragDimensionToDimension(from: string, to: string) { + async dragDimensionToDimension({ + from, + to, + through, + }: { + from: string; + to: string; + through?: string; + }) { await find.existsByCssSelector(from); + await find.existsByCssSelector(to); await browser.html5DragAndDrop( testSubjects.getCssSelector(from), - testSubjects.getCssSelector(to) + testSubjects.getCssSelector(to), + through && testSubjects.getCssSelector(through) ); await this.waitForLensDragDropToFinish(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -891,7 +898,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont return dimensionTexts[index]; }, /** - * Gets label of all dimension triggers in dimension group + * Gets label of all dimension triggers in an element * * @param dimension - the selector of the dimension */