[Lens] Implement drag and drop between layers (#132018)

* added dnd between layers

* Andrew's comments addressed

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Marta Bondyra 2022-06-23 09:32:19 +02:00 committed by GitHub
parent bf65d4c261
commit d11c0be465
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 5519 additions and 3586 deletions

View file

@ -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 } }}
>
<button>Hello!</button>
</DragDrop>
@ -231,7 +238,7 @@ describe('DragDrop', () => {
}}
>
<DragDrop
value={{ id: '3', humanData: { label: 'ignored' } }}
value={{ id: '3', humanData: { label: 'ignored', layerNumber: 0 } }}
draggable={true}
order={[2, 0, 1, 0]}
>
@ -286,7 +293,7 @@ describe('DragDrop', () => {
registerDropTarget={jest.fn()}
>
<DragDrop
value={{ id: '3', humanData: { label: 'ignored' } }}
value={{ id: '3', humanData: { label: 'ignored', layerNumber: 0 } }}
draggable={true}
order={[2, 0, 1, 0]}
>
@ -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(
<ChildDragDropProvider
setA11yMessage={jest.fn()}
dragging={{ id: '1', humanData: { label: 'Label1' } }}
dragging={{ id: '1', humanData: { label: 'Label1', layerNumber: 0 } }}
setDragging={jest.fn()}
setActiveDropTarget={setActiveDropTarget}
activeDropTarget={activeDropTarget}
@ -543,7 +551,7 @@ describe('DragDrop', () => {
registerDropTarget={jest.fn()}
>
<DragDrop
value={{ id: '3', humanData: { label: 'ignored' } }}
value={{ id: '3', humanData: { label: 'ignored', layerNumber: 0 } }}
draggable={true}
order={[2, 0, 1, 0]}
>
@ -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,
},
];

View file

@ -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)
);
},
};

View file

@ -10,6 +10,7 @@ import { DropType } from '../../types';
export interface HumanData {
label: string;
groupLabel?: string;
layerNumber?: number;
position?: number;
nextLabel?: string;
canSwap?: boolean;

View file

@ -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<unknown, unknown>;
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}

View file

@ -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'],
});
});
});
});

View file

@ -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,
GetDropPropsArgs,
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<unknown, unknown>,
dropProps: GetDropProps,
isNew?: boolean
dropProps: GetDropPropsArgs,
sharedDatasource?: Datasource<unknown, unknown>
): { 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<T> {
prevState: T;
target: DragDropOperation;
source: DragDropIdentifier;
frame: FramePublicAPI;
dropType: DropType;
group?: VisualizationDimensionGroupConfig;
}
export function onDropForVisualization<T>(
props: OnVisDropProps<T>,
activeVisualization: Visualization<T>
) {
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;
}

View file

@ -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<unknown, unknown>;
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]
);

View file

@ -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(
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
<LayerPanel
{...getDefaultProps()}
updateVisualization={updateVisualization}
activeVisualization={mockVis}
/>
</ChildDragDropProvider>
);
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', () => {

View file

@ -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 (dropType === 'reorder' || dropType === 'field_replace' || dropType === 'field_add') {
setNextFocusedButtonId(droppedItem.id);
} else {
setNextFocusedButtonId(columnId);
if (!isOperation(target)) {
throw new Error('Drop target should be an operation');
}
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,
});
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);
}
}
if (dropType === 'reorder' || dropType === 'field_replace' || dropType === 'field_add') {
setNextFocusedButtonId(source.id);
} 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);
}
setNextFocusedButtonId(target.columnId);
}
let hasDropSucceeded = true;
if (layerDatasource) {
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);
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 (
<DraggableDimensionButton
registerNewButtonRef={registerNewButtonRef}
accessorIndex={accessorIndex}
columnId={columnId}
group={group}
groups={groups}
accessorIndex={accessorIndex}
groupIndex={groupIndex}
key={columnId}
layerDatasourceDropProps={layerDatasourceDropProps}
state={layerDatasourceState}
label={columnLabelMap?.[columnId]}
layerDatasource={layerDatasource}
datasourceLayers={framePublicAPI.datasourceLayers}
layerIndex={layerIndex}
layerId={layerId}
onDragStart={() => setHideTooltip(true)}
@ -562,12 +527,12 @@ export function LayerPanel(
{group.supportsMoreColumns ? (
<EmptyDimensionButton
group={group}
groupIndex={groupIndex}
groups={groups}
layerId={layerId}
groupIndex={groupIndex}
layerIndex={layerIndex}
layerDatasource={layerDatasource}
layerDatasourceDropProps={layerDatasourceDropProps}
state={layerDatasourceState}
datasourceLayers={framePublicAPI.datasourceLayers}
onClick={(id) => {
props.onEmptyDimensionAdd(id, group);
setActiveDimension({

View file

@ -29,7 +29,6 @@ export interface LayerPanelProps {
}
export interface LayerDatasourceDropProps {
layerId: string;
state: unknown;
setState: (newState: unknown) => void;
}

View file

@ -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',
]);
});
});
});

View file

@ -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,
DataViewDragDropOperation,
} from '../../types';
import {
getDropPropsForSameGroup,
isOperationFromTheSameGroup,
} from '../../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils';
type GetDropProps = DatasourceDimensionDropProps<IndexPatternPrivateState> & {
dragging?: DragContextState['dragging'];
groupId: string;
};
interface GetDropPropsArgs {
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) {
export function getDropProps(props: GetDropPropsArgs) {
const { state, source, target } = props;
if (!source) {
return;
}
const targetProps: DataViewDragDropOperation = {
...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: DataViewDragDropOperation = {
...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,
}: GetDropPropsArgs & { 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: DataViewDragDropOperation,
targetProps: DataViewDragDropOperation
): 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: DataViewDragDropOperation,
targetProps: DataViewDragDropOperation
): 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 {

View file

@ -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<string, IndexPattern> => {
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<string, GenericIndexPatternColumn> = {
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<string, (...args: string[]) => 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,
},
};

View file

@ -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,75 +21,461 @@ 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, DataViewDragDropOperation } from '../../types';
import { trackUiEvent } from '../../../lens_ui_telemetry';
type DropHandlerProps<T> = DatasourceDimensionDropHandlerProps<IndexPatternPrivateState> & {
droppedItem: T;
};
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
const { droppedItem, dropType } = props;
if (dropType === 'field_add' || dropType === 'field_replace' || dropType === 'field_combine') {
return operationOnDropMap[dropType]({
...props,
droppedItem: droppedItem as DraggedField,
});
}
return operationOnDropMap[dropType]({
...props,
droppedItem: droppedItem as DraggedOperation,
});
interface DropHandlerProps<T = DataViewDragDropOperation> {
state: IndexPatternPrivateState;
setState: StateSetter<
IndexPatternPrivateState,
{
isDimensionComplete?: boolean;
forceRender?: boolean;
}
>;
dimensionGroups: VisualizationDimensionGroupConfig[];
dropType?: DropType;
source: T;
target: DataViewDragDropOperation;
}
const operationOnDropMap = {
field_add: onFieldDrop,
field_replace: onFieldDrop,
field_combine: (props: DropHandlerProps<DraggedField>) => onFieldDrop(props, true),
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
const { target, source, dropType, state } = props;
reorder: onReorder,
if (isDraggedField(source) && isFieldDropType(dropType)) {
return onFieldDrop(
{
...props,
target: {
...target,
dataView: state.indexPatterns[state.layers[target.layerId].indexPatternId],
},
source,
},
dropType === 'field_combine'
);
}
move_compatible: (props: DropHandlerProps<DraggedOperation>) => onMoveCompatible(props, true),
replace_compatible: (props: DropHandlerProps<DraggedOperation>) => onMoveCompatible(props, true),
duplicate_compatible: onMoveCompatible,
replace_duplicate_compatible: onMoveCompatible,
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;
}
move_incompatible: (props: DropHandlerProps<DraggedOperation>) => onMoveIncompatible(props, true),
replace_incompatible: (props: DropHandlerProps<DraggedOperation>) =>
onMoveIncompatible(props, true),
duplicate_incompatible: onMoveIncompatible,
replace_duplicate_incompatible: onMoveIncompatible,
const operationProps = {
...props,
target: {
...target,
dataView: targetDataView,
},
source: {
...source,
dataView: sourceDataView,
},
};
if (dropType === 'reorder') {
return onReorder(operationProps);
}
swap_compatible: onSwapCompatible,
swap_incompatible: onSwapIncompatible,
combine_compatible: onCombineCompatible,
combine_incompatible: onCombineCompatible,
};
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);
}
}
function onCombineCompatible({
columnId,
const isFieldDropType = (dropType: DropType) =>
['field_add', 'field_replace', 'field_combine'].includes(dropType);
function onFieldDrop(props: DropHandlerProps<DraggedField>, shouldAddField?: boolean) {
const { setState, state, source, target, dimensionGroups } = props;
const prioritizedOperation = dimensionGroups.find(
(g) => g.groupId === target.groupId
)?.prioritizedOperation;
const layer = state.layers[target.layerId];
const indexPattern = state.indexPatterns[layer.indexPatternId];
const targetColumn = layer.columns[target.columnId];
const newOperation = shouldAddField
? targetColumn.operationType
: getNewOperation(source.field, target.filterOperations, targetColumn, prioritizedOperation);
if (
!isDraggedField(source) ||
!newOperation ||
(shouldAddField &&
!hasOperationSupportForMultipleFields(indexPattern, targetColumn, undefined, source.field))
) {
return false;
}
const field = shouldAddField ? getField(targetColumn, indexPattern) : source.field;
const initialParams = shouldAddField
? {
params:
getOperationHelperForMultipleFields(targetColumn.operationType)?.({
targetColumn,
field: source.field,
indexPattern,
}) || {},
}
: undefined;
const newLayer = insertOrReplaceColumn({
layer,
columnId: target.columnId,
indexPattern,
op: newOperation,
field,
visualizationGroups: dimensionGroups,
targetGroup: target.groupId,
shouldCombineField: shouldAddField,
initialParams,
});
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: target.layerId, newLayer }));
return true;
}
function onMoveCompatible(
{ setState, state, source, target, dimensionGroups }: DropHandlerProps<DataViewDragDropOperation>,
shouldDeleteSource?: boolean
) {
const modifiedLayers = copyColumn({
layers: state.layers,
target,
source,
shouldDeleteSource,
});
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 }));
return true;
}
}
function onReorder({
setState,
state,
layerId,
droppedItem,
dimensionGroups,
groupId,
}: DropHandlerProps<DraggedOperation>) {
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];
source,
target,
}: DropHandlerProps<DataViewDragDropOperation>) {
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: target.layerId,
newLayer: {
columnOrder: reorderElements(
state.layers[target.layerId].columnOrder,
target.columnId,
source.columnId
),
},
})
);
return true;
}
function onMoveIncompatible(
{ setState, state, source, dimensionGroups, target }: DropHandlerProps<DataViewDragDropOperation>,
shouldDeleteSource?: boolean
) {
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, target.filterOperations, targetColumn);
if (!newOperation) {
return false;
}
const outputSourceLayer = shouldDeleteSource
? deleteColumn({
layer: sourceLayer,
columnId: source.columnId,
indexPattern,
})
: sourceLayer;
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: 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({
setState,
state,
source,
dimensionGroups,
target,
}: DropHandlerProps<DragDropOperation>) {
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];
// extract the field from the source column
const sourceField = getField(sourceColumn, indexPattern);
const targetField = getField(targetColumn, indexPattern);
const newOperationForSource = getNewOperation(sourceField, target.filterOperations, targetColumn);
const newOperationForTarget = getNewOperation(targetField, source.filterOperations, sourceColumn);
if (!newOperationForSource || !newOperationForTarget) {
return false;
}
const outputTargetLayer = insertOrReplaceColumn({
layer: targetLayer,
columnId: target.columnId,
targetGroup: target.groupId,
indexPattern,
op: newOperationForSource,
field: sourceField,
visualizationGroups: dimensionGroups,
shouldResetLabel: 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 sourceIndex = columnOrder.findIndex((c) => c === sourceId);
const targetIndex = columnOrder.findIndex((c) => c === targetId);
const newColumnOrder = [...columnOrder];
newColumnOrder[sourceIndex] = targetId;
newColumnOrder[targetIndex] = sourceId;
return newColumnOrder;
};
function onSwapCompatible({
setState,
state,
source,
dimensionGroups,
target,
}: DropHandlerProps<DataViewDragDropOperation>) {
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<DataViewDragDropOperation>) {
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;
}
@ -96,339 +489,22 @@ function onCombineCompatible({
}) ?? {},
};
const modifiedLayer = replaceColumn({
layer,
columnId,
const outputTargetLayer = replaceColumn({
layer: targetLayer,
columnId: target.columnId,
indexPattern,
op: targetColumn.operationType,
field: targetField,
visualizationGroups: dimensionGroups,
targetGroup: groupId,
targetGroup: target.groupId,
initialParams,
shouldCombineField: true,
});
const newLayer = deleteColumn({
layer: modifiedLayer,
columnId: sourceId,
indexPattern,
const newLayers = deleteColumnInLayers({
layers: { ...state.layers, [target.layerId]: outputTargetLayer },
source,
});
// Time to replace
setState(
mergeLayer({
state,
layerId,
newLayer,
})
);
return { deleted: sourceId };
}
function onFieldDrop(props: DropHandlerProps<DraggedField>, shouldAddField?: boolean) {
const {
columnId,
setState,
state,
layerId,
droppedItem,
filterOperations,
groupId,
dimensionGroups,
} = props;
const prioritizedOperation = dimensionGroups.find(
(g) => g.groupId === groupId
)?.prioritizedOperation;
const layer = state.layers[layerId];
const indexPattern = state.indexPatterns[layer.indexPatternId];
const targetColumn = layer.columns[columnId];
const newOperation = shouldAddField
? targetColumn.operationType
: getNewOperation(droppedItem.field, filterOperations, targetColumn, prioritizedOperation);
if (
!isDraggedField(droppedItem) ||
!newOperation ||
(shouldAddField &&
!hasOperationSupportForMultipleFields(
indexPattern,
targetColumn,
undefined,
droppedItem.field
))
) {
return false;
}
const field = shouldAddField ? getField(targetColumn, indexPattern) : droppedItem.field;
const initialParams = shouldAddField
? {
params:
getOperationHelperForMultipleFields(targetColumn.operationType)?.({
targetColumn,
field: droppedItem.field,
indexPattern,
}) || {},
}
: undefined;
const newLayer = insertOrReplaceColumn({
layer,
columnId,
indexPattern,
op: newOperation,
field,
visualizationGroups: dimensionGroups,
targetGroup: groupId,
shouldCombineField: shouldAddField,
initialParams,
});
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 }));
return true;
}
function onMoveCompatible(
{
columnId,
setState,
state,
layerId,
droppedItem,
dimensionGroups,
groupId,
}: DropHandlerProps<DraggedOperation>,
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,
shouldDeleteSource,
indexPattern,
});
const updatedColumnOrder = reorderByGroups(
dimensionGroups,
groupId,
getColumnOrder(modifiedLayer),
columnId
);
// Time to replace
setState(
mergeLayer({
state,
layerId,
newLayer: {
columnOrder: updatedColumnOrder,
columns: modifiedLayer.columns,
},
})
);
return shouldDeleteSource ? { deleted: droppedItem.columnId } : true;
}
function onReorder({
columnId,
setState,
state,
layerId,
droppedItem,
}: DropHandlerProps<DraggedOperation>) {
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);
return result;
}
setState(
mergeLayer({
state,
layerId,
newLayer: {
columnOrder: reorderElements(
state.layers[layerId].columnOrder,
columnId,
droppedItem.columnId
),
},
})
);
return true;
}
function onMoveIncompatible(
{
columnId,
setState,
state,
layerId,
droppedItem,
filterOperations,
dimensionGroups,
groupId,
}: DropHandlerProps<DraggedOperation>,
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 sourceField = getField(sourceColumn, indexPattern);
const newOperation = getNewOperation(sourceField, filterOperations, targetColumn);
if (!newOperation) {
return false;
}
const modifiedLayer = shouldDeleteSource
? deleteColumn({
layer,
columnId: droppedItem.columnId,
indexPattern,
})
: layer;
const newLayer = insertOrReplaceColumn({
layer: modifiedLayer,
columnId,
indexPattern,
op: newOperation,
field: sourceField,
visualizationGroups: dimensionGroups,
targetGroup: groupId,
shouldResetLabel: true,
});
trackUiEvent('drop_onto_dimension');
setState(
mergeLayer({
state,
layerId,
newLayer,
})
);
return shouldDeleteSource ? { deleted: droppedItem.columnId } : true;
}
function onSwapIncompatible({
columnId,
setState,
state,
layerId,
droppedItem,
filterOperations,
dimensionGroups,
groupId,
}: DropHandlerProps<DraggedOperation>) {
const layer = state.layers[layerId];
const indexPattern = state.indexPatterns[layer.indexPatternId];
const sourceColumn = layer.columns[droppedItem.columnId];
const targetColumn = layer.columns[columnId];
const sourceField = getField(sourceColumn, indexPattern);
const targetField = getField(targetColumn, indexPattern);
const newOperationForSource = getNewOperation(sourceField, filterOperations, targetColumn);
const newOperationForTarget = getNewOperation(
targetField,
droppedItem.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,
indexPattern,
op: newOperationForTarget,
field: targetField,
visualizationGroups: dimensionGroups,
targetGroup: droppedItem.groupId,
shouldResetLabel: true,
});
trackUiEvent('drop_onto_dimension');
setState(
mergeLayer({
state,
layerId,
newLayer,
})
);
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);
newColumnOrder[sourceIndex] = targetId;
newColumnOrder[targetIndex] = sourceId;
return newColumnOrder;
};
function onSwapCompatible({
columnId,
setState,
state,
layerId,
droppedItem,
dimensionGroups,
groupId,
}: DropHandlerProps<DraggedOperation>) {
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,
},
})
);
setState(mergeLayers({ state, newLayers }));
return true;
}

View file

@ -18,9 +18,9 @@ export interface OperationSupportMatrix {
}
type Props = Pick<
DatasourceDimensionDropProps<IndexPatternPrivateState>,
'layerId' | 'columnId' | 'state' | 'filterOperations'
>;
DatasourceDimensionDropProps<IndexPatternPrivateState>['target'],
'layerId' | 'columnId' | 'filterOperations'
> & { state: IndexPatternPrivateState };
function computeOperationMatrix(
operationsByMetadata: Array<{

View file

@ -150,12 +150,8 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
},
];
},
onOtherColumnChanged: (layer, thisColumnId, changedColumnId) =>
adjustTimeScaleOnOtherColumnChange<CountIndexPatternColumn>(
layer,
thisColumnId,
changedColumnId
),
onOtherColumnChanged: (layer, thisColumnId) =>
adjustTimeScaleOnOtherColumnChange<CountIndexPatternColumn>(layer, thisColumnId),
toEsAggsFn: (column, columnId) => {
return buildExpressionFunction<AggFunctionsMapping['aggCount']>('aggCount', {
id: columnId,

View file

@ -174,13 +174,23 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
isTransferable: () => {
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,

View file

@ -67,8 +67,8 @@ export const mathOperation: OperationDefinition<MathIndexPatternColumn, 'managed
// TODO has to check all children
return true;
},
createCopy: (layer) => {
return { ...layer };
createCopy: (layers) => {
return { ...layers };
},
};

View file

@ -54,7 +54,12 @@ import type {
GenericIndexPatternColumn,
ReferenceBasedIndexPatternColumn,
} from './column_types';
import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types';
import {
DataViewDragDropOperation,
IndexPattern,
IndexPatternField,
IndexPatternLayer,
} from '../../types';
import { DateRange, LayerType } from '../../../../common';
import { rangeOperation } from './ranges';
import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from '../../dimension_panel';
@ -249,11 +254,7 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn, P = {}>
* Based on the current column and the other updated columns, this function has to
* return an updated column. If not implemented, the `id` function is used instead.
*/
onOtherColumnChanged?: (
layer: IndexPatternLayer,
thisColumnId: string,
changedColumnId: string
) => C;
onOtherColumnChanged?: (layer: IndexPatternLayer, thisColumnId: string) => C;
/**
* React component for operation specific settings shown in the flyout editor
*/
@ -623,12 +624,11 @@ interface ManagedReferenceOperationDefinition<C extends BaseIndexPatternColumn>
* root level
*/
createCopy: (
layer: IndexPatternLayer,
sourceColumnId: string,
targetColumnId: string,
indexPattern: IndexPattern,
layers: Record<string, IndexPatternLayer>,
source: DataViewDragDropOperation,
target: DataViewDragDropOperation,
operationDefinitionMap: Record<string, GenericOperationDefinition>
) => IndexPatternLayer;
) => Record<string, IndexPatternLayer>;
}
interface OperationDefinitionMap<C extends BaseIndexPatternColumn, P = {}> {

View file

@ -108,9 +108,9 @@ function buildMetricOperation<T extends MetricColumn<string>>({
(!newField.aggregationRestrictions || newField.aggregationRestrictions![type])
);
},
onOtherColumnChanged: (layer, thisColumnId, changedColumnId) =>
onOtherColumnChanged: (layer, thisColumnId) =>
optionalTimeScaling
? (adjustTimeScaleOnOtherColumnChange(layer, thisColumnId, changedColumnId) as T)
? (adjustTimeScaleOnOtherColumnChange(layer, thisColumnId) as T)
: (layer.columns[thisColumnId] as T),
getDefaultLabel: (column, indexPattern, columns) =>
labelLookup(getSafeName(column.sourceField, indexPattern), column),

View file

@ -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 }),
},
};
},

View file

@ -325,7 +325,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
params: newParams,
};
},
onOtherColumnChanged: (layer, thisColumnId, changedColumnId) => {
onOtherColumnChanged: (layer, thisColumnId) => {
const columns = layer.columns;
const currentColumn = columns[thisColumnId] as TermsIndexPatternColumn;
if (

View file

@ -755,8 +755,7 @@ describe('terms', () => {
},
},
},
'col2',
'col1'
'col2'
);
expect(updatedColumn).toBe(initialColumn);
@ -796,8 +795,7 @@ describe('terms', () => {
columnOrder: [],
indexPatternId: '',
},
'col2',
'col1'
'col2'
);
expect(updatedColumn.params).toEqual(
expect.objectContaining({
@ -843,8 +841,7 @@ describe('terms', () => {
columnOrder: [],
indexPatternId: '',
},
'col2',
'col1'
'col2'
);
expect(updatedColumn.params).toEqual(
expect.objectContaining({
@ -875,8 +872,7 @@ describe('terms', () => {
columnOrder: [],
indexPatternId: '',
},
'col2',
'col1'
'col2'
);
expect(termsColumn.params).toEqual(
expect.objectContaining({
@ -919,8 +915,7 @@ describe('terms', () => {
columnOrder: [],
indexPatternId: '',
},
'col2',
'col1'
'col2'
);
expect(termsColumn.params).toEqual(
expect.objectContaining({
@ -951,8 +946,7 @@ describe('terms', () => {
columnOrder: [],
indexPatternId: '',
},
'col2',
'col1'
'col2'
);
expect(termsColumn.params).toEqual(
expect.objectContaining({
@ -991,8 +985,7 @@ describe('terms', () => {
},
},
},
'col2',
'col1'
'col2'
);
expect(updatedColumn.params).toEqual(

View file

@ -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: [
@ -1355,8 +1369,7 @@ describe('state_helpers', () => {
},
incompleteColumns: {},
},
'col1',
'col2'
'col1'
);
});
@ -1422,8 +1435,7 @@ describe('state_helpers', () => {
},
incompleteColumns: {},
}),
'col1',
'willBeReference'
'col1'
);
});
@ -2374,8 +2386,7 @@ describe('state_helpers', () => {
expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith(
{ indexPatternId: '1', columnOrder: ['col1', 'col2'], columns: { col1: termsColumn } },
'col1',
'col2'
'col1'
);
});

View file

@ -26,6 +26,7 @@ import {
TermsIndexPatternColumn,
} from './definitions';
import type {
DataViewDragDropOperation,
IndexPattern,
IndexPatternField,
IndexPatternLayer,
@ -68,96 +69,84 @@ interface ColumnChange {
}
interface ColumnCopy {
layer: IndexPatternLayer;
targetId: string;
sourceColumn: GenericIndexPatternColumn;
sourceColumnId: string;
indexPattern: IndexPattern;
layers: Record<string, IndexPatternLayer>;
target: DataViewDragDropOperation;
source: DataViewDragDropOperation;
shouldDeleteSource?: boolean;
}
export const deleteColumnInLayers = ({
layers,
source,
}: {
layers: Record<string, IndexPatternLayer>;
source: DataViewDragDropOperation;
}) => ({
...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<string, IndexPatternLayer> {
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<string, IndexPatternLayer>,
target: DataViewDragDropOperation,
source: DataViewDragDropOperation
): Record<string, IndexPatternLayer> {
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<C extends GenericIndexPatternColumn>({
};
}
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
@ -1196,8 +1205,7 @@ export function adjustColumnReferencesForChangedColumn(
newColumns[currentColumnId] = operationDefinition.onOtherColumnChanged
? operationDefinition.onOtherColumnChanged(
{ ...layer, columns: newColumns },
currentColumnId,
changedColumnId
currentColumnId
)
: currentColumn;
}
@ -1561,6 +1569,9 @@ export function isColumnValidAsReference({
if (!column) return false;
const operationType = column.operationType;
const operationDefinition = operationDefinitionMap[operationType];
if (!operationDefinition) {
throw new Error('No suitable operation definition found for ' + operationType);
}
return (
validation.input.includes(operationDefinition.input) &&
maybeValidateOperations({

View file

@ -113,11 +113,7 @@ describe('time scale utils', () => {
it('should keep column if there is no time scale', () => {
const column = { ...baseColumn, timeScale: undefined };
expect(
adjustTimeScaleOnOtherColumnChange(
{ ...baseLayer, columns: { col1: column } },
'col1',
'col2'
)
adjustTimeScaleOnOtherColumnChange({ ...baseLayer, columns: { col1: column } }, 'col1')
).toBe(column);
});
@ -138,14 +134,13 @@ describe('time scale utils', () => {
} as DateHistogramIndexPatternColumn,
},
},
'col1',
'col2'
'col1'
)
).toBe(baseColumn);
});
it('should remove time scale if there is no date histogram', () => {
expect(adjustTimeScaleOnOtherColumnChange(baseLayer, 'col1', 'col2')).toHaveProperty(
expect(adjustTimeScaleOnOtherColumnChange(baseLayer, 'col1')).toHaveProperty(
'timeScale',
undefined
);
@ -153,22 +148,14 @@ describe('time scale utils', () => {
it('should remove suffix from label', () => {
expect(
adjustTimeScaleOnOtherColumnChange(
{ ...baseLayer, columns: { col1: baseColumn } },
'col1',
'col2'
)
adjustTimeScaleOnOtherColumnChange({ ...baseLayer, columns: { col1: baseColumn } }, 'col1')
).toHaveProperty('label', 'Count of records');
});
it('should keep custom label', () => {
const column = { ...baseColumn, label: 'abc', customLabel: true };
expect(
adjustTimeScaleOnOtherColumnChange(
{ ...baseLayer, columns: { col1: column } },
'col1',
'col2'
)
adjustTimeScaleOnOtherColumnChange({ ...baseLayer, columns: { col1: column } }, 'col1')
).toHaveProperty('label', 'abc');
});
});

View file

@ -46,8 +46,7 @@ export function adjustTimeScaleLabelSuffix(
export function adjustTimeScaleOnOtherColumnChange<T extends GenericIndexPatternColumn>(
layer: IndexPatternLayer,
thisColumnId: string,
changedColumnId: string
thisColumnId: string
): T {
const columns = layer.columns;
const column = columns[thisColumnId] as T;

View file

@ -24,3 +24,19 @@ export function mergeLayer({
},
};
}
export function mergeLayers({
state,
newLayers,
}: {
state: IndexPatternPrivateState;
newLayers: Record<string, IndexPatternLayer>;
}) {
return {
...state,
layers: {
...state.layers,
...newLayers,
},
};
}

View file

@ -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 DataViewDragDropOperation extends DragDropOperation {
dataView: IndexPattern;
column?: GenericIndexPatternColumn;
}

View file

@ -199,11 +199,18 @@ interface ChartSettings {
};
}
export type GetDropProps<T = unknown> = DatasourceDimensionDropProps<T> & {
groupId: string;
dragging: DragContextState['dragging'];
prioritizedOperation?: string;
};
export interface GetDropPropsArgs<T = unknown> {
state: T;
source?: DraggingIdentifier;
target: {
layerId: string;
groupId: string;
columnId: string;
filterOperations: (meta: OperationMetadata) => boolean;
prioritizedOperation?: string;
isNewColumn?: boolean;
};
}
/**
* Interface for the datasource registry
@ -257,9 +264,9 @@ export interface Datasource<T = unknown, P = unknown> {
props: DatasourceLayerPanelProps<T>
) => ((cleanupElement: Element) => void) | void;
getDropProps: (
props: GetDropProps<T>
props: GetDropPropsArgs<T>
) => { dropTypes: DropType[]; nextLabel?: string } | undefined;
onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => false | true | { deleted: string };
onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => 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<T> {
activeData?: Record<string, Datatable>;
}
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<T> = SharedDimensionProps & {
layerId: string;
groupId: string;
columnId: string;
export interface DatasourceDimensionDropProps<T> {
target: DragDropOperation;
state: T;
setState: StateSetter<
T,
@ -484,10 +487,10 @@ export type DatasourceDimensionDropProps<T> = SharedDimensionProps & {
}
>;
dimensionGroups: VisualizationDimensionGroupConfig[];
};
}
export type DatasourceDimensionDropHandlerProps<T> = DatasourceDimensionDropProps<T> & {
droppedItem: unknown;
export type DatasourceDimensionDropHandlerProps<S> = DatasourceDimensionDropProps<S> & {
source: DragDropIdentifier;
dropType: DropType;
};
@ -851,7 +854,17 @@ export interface Visualization<T = unknown> {
* look at its internal state to determine which dimension is being affected.
*/
removeDimension: (props: VisualizationDimensionChangeProps<T>) => T;
/**
* Allow defining custom behavior for the visualization when the drop action occurs.
*/
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
*/

View file

@ -118,6 +118,207 @@ export const getAnnotationsSupportedLayer = (
};
};
const getDefaultAnnotationConfig = (id: string, timestamp: string): EventAnnotationConfig => ({
label: defaultAnnotationLabel,
key: {
type: 'point_in_time',
timestamp,
},
icon: 'triangle',
id,
});
const createCopiedAnnotation = (
newId: string,
timestamp: string,
source?: EventAnnotationConfig
): EventAnnotationConfig => {
if (!source) {
return getDefaultAnnotationConfig(newId, timestamp);
}
return {
...source,
id: newId,
};
};
export const onAnnotationDrop: Visualization<XYState>['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,
createCopiedAnnotation(
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
? createCopiedAnnotation(
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<XYState>['setDimension'] = ({
prevState,
layerId,
@ -125,46 +326,30 @@ export const setAnnotationsDimension: Visualization<XYState>['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,
createCopiedAnnotation(
columnId,
getStaticDate(getDataLayers(prevState.layers), frame),
sourceAnnotation
),
],
}
: l
),
};
};
@ -224,7 +409,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,

View file

@ -18,6 +18,7 @@ import {
checkScaleOperation,
getAxisName,
getDataLayers,
getReferenceLayers,
isNumericMetric,
isReferenceLayer,
} from './visualization_helpers';
@ -342,7 +343,10 @@ export const setReferenceDimension: Visualization<XYState>['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 =

View file

@ -476,7 +476,7 @@ describe('xy_visualization', () => {
});
it('should copy previous column if passed and assign a new id', () => {
expect(
xyVisualization.setDimension({
xyVisualization.onDrop!({
frame,
prevState: {
...exampleState(),
@ -488,10 +488,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',
@ -501,7 +511,7 @@ describe('xy_visualization', () => {
});
it('should reorder a dimension to a annotation layer', () => {
expect(
xyVisualization.setDimension({
xyVisualization.onDrop!({
frame,
prevState: {
...exampleState(),
@ -513,10 +523,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',
@ -524,6 +545,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],
},
]);
});
});
});

View file

@ -27,7 +27,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';
@ -45,6 +45,7 @@ import {
getAnnotationsSupportedLayer,
setAnnotationsDimension,
getUniqueLabels,
onAnnotationDrop,
} from './annotations/helpers';
import {
checkXAccessorCompatibility,
@ -71,6 +72,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 = ({
datatableUtilities,
@ -303,6 +305,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) {
throw new Error('target layer should exist');
}
if (isAnnotationsLayer(targetLayer)) {
return onAnnotationDrop?.(props) || props.prevState;
}
return onDropForVisualization(props, this);
},
setDimension(props) {
const { prevState, layerId, columnId, groupId } = props;

View file

@ -334,6 +334,7 @@ export function validateLayersForDimension(
export const isNumericMetric = (op: OperationMetadata) =>
!op.isBucketed && op.dataType === 'number';
export const isNumericDynamicMetric = (op: OperationMetadata) =>
isNumericMetric(op) && !op.isStaticValue;
export const isBucketed = (op: OperationMetadata) => op.isBucketed;

View file

@ -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 despace 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 despace 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 despace 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 despace 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",

View file

@ -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",

View file

@ -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 键",

View file

@ -146,23 +146,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.createLayer();
await PageObjects.lens.switchToVisualization('area');
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
},
1
);
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'median',
field: 'bytes',
},
1
);
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'median',
field: 'bytes',
});
await a11y.testAppSnapshot();
});

View file

@ -125,23 +125,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false);
await PageObjects.lens.switchToVisualization('line');
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
},
1
);
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
});
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'median',
field: 'bytes',
},
1
);
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'median',
field: 'bytes',
});
expect(await PageObjects.lens.getLayerCount()).to.eql(2);
await PageObjects.lens.removeLayer();
@ -308,23 +302,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.createLayer();
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
},
1
);
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
});
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'average',
field: 'bytes',
},
1
);
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'average',
field: 'bytes',
});
await PageObjects.lens.save('twolayerchart');
await testSubjects.click('lnsSuggestion-asDonut > lnsSuggestion');

View file

@ -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'

View file

@ -183,23 +183,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false);
await PageObjects.lens.switchToVisualization('line');
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
},
1
);
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'median',
field: 'bytes',
},
1
);
await PageObjects.lens.configureDimension({
dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'median',
field: 'bytes',
});
await PageObjects.lens.saveAndReturn();
await panelActions.openContextMenu();

View file

@ -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(

View file

@ -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,132 @@ 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.dragDimensionToExtraDropType(
'lns-layerPanel-0 > lnsXY_xDimensionPanel > lns-dimensionTrigger',
'lns-layerPanel-1 > lnsXY_xDimensionPanel',
'duplicate'
);
await PageObjects.lens.assertFocusedDimension('@timestamp [1]');
await PageObjects.lens.dragDimensionToExtraDropType(
'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-dimensionTrigger',
'lns-layerPanel-1 > lnsXY_yDimensionPanel',
'duplicate'
);
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.dragDimensionToExtraDropType(
'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-dimensionTrigger',
'lns-layerPanel-1 > lnsXY_yDimensionPanel',
'duplicate'
);
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.dragDimensionToExtraDropType(
'lns-layerPanel-1 > lnsXY_splitDimensionPanel > lns-dimensionTrigger',
'lns-layerPanel-0 > lnsXY_splitDimensionPanel',
'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.dragDimensionToExtraDropType(
'lns-layerPanel-0 > lnsXY_splitDimensionPanel > lns-dimensionTrigger',
'lns-layerPanel-1 > lnsXY_splitDimensionPanel',
'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',
]);
});
});
});
}

View file

@ -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();

View file

@ -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');
});

View file

@ -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(

View file

@ -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,8 +447,9 @@ 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 }: { from: string; to: string }) {
await find.existsByCssSelector(from);
await find.existsByCssSelector(to);
await browser.html5DragAndDrop(
testSubjects.getCssSelector(from),
testSubjects.getCssSelector(to)
@ -891,7 +889,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
*/