mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Lens] Refactor reorder drag and drop (#88578)
This commit is contained in:
parent
e31b6a8c91
commit
1b8c3c1dcc
39 changed files with 2098 additions and 1176 deletions
|
@ -289,13 +289,13 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
|
|||
}
|
||||
|
||||
const origin = document.querySelector(arguments[0]);
|
||||
const target = document.querySelector(arguments[1]);
|
||||
|
||||
const dragStartEvent = createEvent('dragstart');
|
||||
dispatchEvent(origin, dragStartEvent);
|
||||
|
||||
setTimeout(() => {
|
||||
const dropEvent = createEvent('drop');
|
||||
const target = document.querySelector(arguments[1]);
|
||||
dispatchEvent(target, dropEvent, dragStartEvent.dataTransfer);
|
||||
const dragEndEvent = createEvent('dragend');
|
||||
dispatchEvent(origin, dragEndEvent, dropEvent.dataTransfer);
|
||||
|
|
|
@ -13,10 +13,8 @@ exports[`DragDrop items that have droppable=false get special styling when anoth
|
|||
<button
|
||||
className="lnsDragDrop lnsDragDrop-isDroppable lnsDragDrop-isNotDroppable"
|
||||
data-test-subj="lnsDragDrop"
|
||||
onDragEnd={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onDragOver={[Function]}
|
||||
onDragStart={[Function]}
|
||||
onDrop={[Function]}
|
||||
>
|
||||
Hello!
|
||||
|
@ -24,11 +22,19 @@ exports[`DragDrop items that have droppable=false get special styling when anoth
|
|||
`;
|
||||
|
||||
exports[`DragDrop renders if nothing is being dragged 1`] = `
|
||||
<button
|
||||
class="lnsDragDrop lnsDragDrop-isDraggable"
|
||||
data-test-subj="lnsDragDrop"
|
||||
draggable="true"
|
||||
>
|
||||
Hello!
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
aria-describedby="lnsDragDrop-keyboardInstructions"
|
||||
aria-label="dragging"
|
||||
class="euiScreenReaderOnly--showOnFocus lnsDragDrop__keyboardHandler"
|
||||
data-test-subj="lnsDragDrop-keyboardHandler"
|
||||
/>
|
||||
<button
|
||||
class="lnsDragDrop lnsDragDrop-isDraggable"
|
||||
data-test-subj="lnsDragDrop"
|
||||
draggable="true"
|
||||
>
|
||||
Hello!
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.lnsDragDrop__reorderableContainer {
|
||||
.lnsDragDrop__container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -63,11 +63,18 @@
|
|||
height: calc(100% + #{$lnsLayerPanelDimensionMargin});
|
||||
}
|
||||
|
||||
.lnsDragDrop-isReorderable {
|
||||
.lnsDragDrop-translatableDrop {
|
||||
transform: translateY(0);
|
||||
transition: transform $euiAnimSpeedFast ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lnsDragDrop-translatableDrag {
|
||||
transform: translateY(0);
|
||||
transition: transform $euiAnimSpeedFast ease-in-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Draggable item when it is moving
|
||||
.lnsDragDrop-isHidden {
|
||||
opacity: 0;
|
||||
|
|
|
@ -6,11 +6,33 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render, mount } from 'enzyme';
|
||||
import { DragDrop, ReorderableDragDrop, DropToHandler, DropHandler } from './drag_drop';
|
||||
import { ChildDragDropProvider, ReorderProvider } from './providers';
|
||||
import { DragDrop, DropHandler } from './drag_drop';
|
||||
import {
|
||||
ChildDragDropProvider,
|
||||
DragContextState,
|
||||
ReorderProvider,
|
||||
DragDropIdentifier,
|
||||
ActiveDropTarget,
|
||||
} from './providers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
const defaultContext = {
|
||||
dragging: undefined,
|
||||
setDragging: jest.fn(),
|
||||
setActiveDropTarget: () => {},
|
||||
activeDropTarget: undefined,
|
||||
keyboardMode: false,
|
||||
setKeyboardMode: () => {},
|
||||
setA11yMessage: jest.fn(),
|
||||
};
|
||||
|
||||
const dataTransfer = {
|
||||
setData: jest.fn(),
|
||||
getData: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DragDrop', () => {
|
||||
const value = { id: '1', label: 'hello' };
|
||||
test('renders if nothing is being dragged', () => {
|
||||
|
@ -26,7 +48,7 @@ describe('DragDrop', () => {
|
|||
test('dragover calls preventDefault if droppable is true', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const component = mount(
|
||||
<DragDrop droppable>
|
||||
<DragDrop droppable value={value}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
);
|
||||
|
@ -39,7 +61,7 @@ describe('DragDrop', () => {
|
|||
test('dragover does not call preventDefault if droppable is false', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const component = mount(
|
||||
<DragDrop>
|
||||
<DragDrop value={value}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
);
|
||||
|
@ -51,13 +73,9 @@ describe('DragDrop', () => {
|
|||
|
||||
test('dragstart sets dragging in the context', async () => {
|
||||
const setDragging = jest.fn();
|
||||
const dataTransfer = {
|
||||
setData: jest.fn(),
|
||||
getData: jest.fn(),
|
||||
};
|
||||
|
||||
const component = mount(
|
||||
<ChildDragDropProvider dragging={value} setDragging={setDragging}>
|
||||
<ChildDragDropProvider {...defaultContext} dragging={value} setDragging={setDragging}>
|
||||
<DragDrop value={value} draggable={true} label="drag label">
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
|
@ -79,7 +97,11 @@ describe('DragDrop', () => {
|
|||
const onDrop = jest.fn();
|
||||
|
||||
const component = mount(
|
||||
<ChildDragDropProvider dragging={{ id: '2', label: 'hi' }} setDragging={setDragging}>
|
||||
<ChildDragDropProvider
|
||||
{...defaultContext}
|
||||
dragging={{ id: '2', label: 'hi' }}
|
||||
setDragging={setDragging}
|
||||
>
|
||||
<DragDrop onDrop={onDrop} droppable={true} value={value}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
|
@ -93,7 +115,7 @@ describe('DragDrop', () => {
|
|||
expect(preventDefault).toBeCalled();
|
||||
expect(stopPropagation).toBeCalled();
|
||||
expect(setDragging).toBeCalledWith(undefined);
|
||||
expect(onDrop).toBeCalledWith({ id: '2', label: 'hi' });
|
||||
expect(onDrop).toBeCalledWith({ id: '2', label: 'hi' }, { id: '1', label: 'hello' });
|
||||
});
|
||||
|
||||
test('drop function is not called on droppable=false', async () => {
|
||||
|
@ -103,7 +125,7 @@ describe('DragDrop', () => {
|
|||
const onDrop = jest.fn();
|
||||
|
||||
const component = mount(
|
||||
<ChildDragDropProvider dragging={{ id: 'hi' }} setDragging={setDragging}>
|
||||
<ChildDragDropProvider {...defaultContext} dragging={{ id: 'hi' }} setDragging={setDragging}>
|
||||
<DragDrop onDrop={onDrop} droppable={false} value={value}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
|
@ -127,6 +149,7 @@ describe('DragDrop', () => {
|
|||
throw x;
|
||||
}}
|
||||
droppable
|
||||
value={value}
|
||||
>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
|
@ -137,11 +160,11 @@ describe('DragDrop', () => {
|
|||
|
||||
test('items that have droppable=false get special styling when another item is dragged', () => {
|
||||
const component = mount(
|
||||
<ChildDragDropProvider dragging={value} setDragging={() => {}}>
|
||||
<ChildDragDropProvider {...defaultContext} dragging={value}>
|
||||
<DragDrop value={value} draggable={true} label="a">
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
<DragDrop onDrop={(x: unknown) => {}} droppable={false}>
|
||||
<DragDrop onDrop={(x: unknown) => {}} droppable={false} value={{ id: '2' }}>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
</ChildDragDropProvider>
|
||||
|
@ -153,17 +176,69 @@ describe('DragDrop', () => {
|
|||
test('additional styles are reflected in the className until drop', () => {
|
||||
let dragging: { id: '1' } | undefined;
|
||||
const getAdditionalClasses = jest.fn().mockReturnValue('additional');
|
||||
let activeDropTarget;
|
||||
|
||||
const component = mount(
|
||||
<ChildDragDropProvider
|
||||
{...defaultContext}
|
||||
dragging={dragging}
|
||||
setDragging={() => {
|
||||
dragging = { id: '1' };
|
||||
}}
|
||||
setActiveDropTarget={(val) => {
|
||||
activeDropTarget = { activeDropTarget: val };
|
||||
}}
|
||||
activeDropTarget={activeDropTarget}
|
||||
>
|
||||
<DragDrop value={{ label: 'ignored', id: '3' }} draggable={true} label="a">
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
<DragDrop
|
||||
value={value}
|
||||
onDrop={(x: unknown) => {}}
|
||||
droppable
|
||||
getAdditionalClassesOnEnter={getAdditionalClasses}
|
||||
>
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop"]')
|
||||
.first()
|
||||
.simulate('dragstart', { dataTransfer });
|
||||
jest.runAllTimers();
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover');
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop');
|
||||
expect(component.find('.additional')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('additional enter styles are reflected in the className until dragleave', () => {
|
||||
let dragging: { id: '1' } | undefined;
|
||||
const getAdditionalClasses = jest.fn().mockReturnValue('additional');
|
||||
const setActiveDropTarget = jest.fn();
|
||||
|
||||
const component = mount(
|
||||
<ChildDragDropProvider
|
||||
setA11yMessage={jest.fn()}
|
||||
dragging={dragging}
|
||||
setDragging={() => {
|
||||
dragging = { id: '1' };
|
||||
}}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
activeDropTarget={
|
||||
({ activeDropTarget: value } as unknown) as DragContextState['activeDropTarget']
|
||||
}
|
||||
keyboardMode={false}
|
||||
setKeyboardMode={(keyboardMode) => true}
|
||||
>
|
||||
<DragDrop value={{ label: 'ignored', id: '3' }} draggable={true} label="a">
|
||||
<button>Hello!</button>
|
||||
</DragDrop>
|
||||
<DragDrop
|
||||
value={value}
|
||||
onDrop={(x: unknown) => {}}
|
||||
droppable
|
||||
getAdditionalClassesOnEnter={getAdditionalClasses}
|
||||
|
@ -173,10 +248,6 @@ describe('DragDrop', () => {
|
|||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
const dataTransfer = {
|
||||
setData: jest.fn(),
|
||||
getData: jest.fn(),
|
||||
};
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop"]')
|
||||
.first()
|
||||
|
@ -187,37 +258,44 @@ describe('DragDrop', () => {
|
|||
expect(component.find('.additional')).toHaveLength(1);
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave');
|
||||
expect(component.find('.additional')).toHaveLength(0);
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover');
|
||||
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop');
|
||||
expect(component.find('.additional')).toHaveLength(0);
|
||||
expect(setActiveDropTarget).toBeCalledWith(undefined);
|
||||
});
|
||||
|
||||
describe('reordering', () => {
|
||||
const mountComponent = (
|
||||
dragging: { id: '1' } | undefined,
|
||||
onDrop: DropHandler = jest.fn(),
|
||||
dropTo: DropToHandler = jest.fn()
|
||||
) =>
|
||||
mount(
|
||||
<ChildDragDropProvider
|
||||
dragging={{ id: '1' }}
|
||||
setDragging={() => {
|
||||
dragging = { id: '1' };
|
||||
}}
|
||||
>
|
||||
dragContext: Partial<DragContextState> | undefined,
|
||||
onDrop: DropHandler = jest.fn()
|
||||
) => {
|
||||
let dragging = dragContext?.dragging;
|
||||
let keyboardMode = !!dragContext?.keyboardMode;
|
||||
let activeDropTarget = dragContext?.activeDropTarget;
|
||||
const baseContext = {
|
||||
dragging,
|
||||
setDragging: (val?: DragDropIdentifier) => {
|
||||
dragging = val;
|
||||
},
|
||||
keyboardMode,
|
||||
setKeyboardMode: jest.fn((mode) => {
|
||||
keyboardMode = mode;
|
||||
}),
|
||||
setActiveDropTarget: (target?: DragDropIdentifier) => {
|
||||
activeDropTarget = { activeDropTarget: target } as ActiveDropTarget;
|
||||
},
|
||||
activeDropTarget,
|
||||
setA11yMessage: jest.fn(),
|
||||
};
|
||||
return mount(
|
||||
<ChildDragDropProvider {...baseContext} {...dragContext}>
|
||||
<ReorderProvider id="groupId">
|
||||
<DragDrop
|
||||
label="1"
|
||||
draggable
|
||||
droppable
|
||||
droppable={false}
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
itemsInGroup={['1', '2', '3']}
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]}
|
||||
value={{ id: '1' }}
|
||||
onDrop={onDrop}
|
||||
dropTo={dropTo}
|
||||
>
|
||||
<span>1</span>
|
||||
</DragDrop>
|
||||
|
@ -227,12 +305,11 @@ describe('DragDrop', () => {
|
|||
droppable
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
itemsInGroup={['1', '2', '3']}
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]}
|
||||
value={{
|
||||
id: '2',
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
dropTo={dropTo}
|
||||
>
|
||||
<span>2</span>
|
||||
</DragDrop>
|
||||
|
@ -242,132 +319,270 @@ describe('DragDrop', () => {
|
|||
droppable
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
itemsInGroup={['1', '2', '3']}
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }, { id: '3' }]}
|
||||
value={{
|
||||
id: '3',
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
dropTo={dropTo}
|
||||
>
|
||||
<span>3</span>
|
||||
</DragDrop>
|
||||
</ReorderProvider>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
test(`ReorderableDragDrop component doesn't appear for groups of 1 or less`, () => {
|
||||
let dragging;
|
||||
};
|
||||
test(`Inactive reorderable group renders properly`, () => {
|
||||
const component = mountComponent(undefined, jest.fn());
|
||||
expect(component.find('.lnsDragDrop-reorderable')).toHaveLength(3);
|
||||
});
|
||||
|
||||
test(`Reorderable group with lifted element renders properly`, () => {
|
||||
const setDragging = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const component = mountComponent(
|
||||
{ dragging: { id: '1' }, setA11yMessage, setDragging },
|
||||
jest.fn()
|
||||
);
|
||||
act(() => {
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop"]')
|
||||
.first()
|
||||
.simulate('dragstart', { dataTransfer });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(setDragging).toBeCalledWith({ id: '1' });
|
||||
expect(setA11yMessage).toBeCalledWith('You have lifted an item 1 in position 1');
|
||||
expect(
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop-reorderableGroup"]')
|
||||
.hasClass('lnsDragDrop-isActiveGroup')
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test(`Reordered elements get extra styles to show the reorder effect when dragging`, () => {
|
||||
const component = mountComponent({ dragging: { id: '1' } }, jest.fn());
|
||||
|
||||
act(() => {
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop"]')
|
||||
.first()
|
||||
.simulate('dragstart', { dataTransfer });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]')
|
||||
.at(1)
|
||||
.simulate('dragover');
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
|
||||
).toEqual(undefined);
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(0).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-8px)',
|
||||
});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-8px)',
|
||||
});
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]')
|
||||
.at(1)
|
||||
.simulate('dragleave');
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
|
||||
).toEqual(undefined);
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
test(`Dropping an item runs onDrop function`, () => {
|
||||
const setDragging = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const preventDefault = jest.fn();
|
||||
const stopPropagation = jest.fn();
|
||||
const onDrop = jest.fn();
|
||||
|
||||
const component = mountComponent(
|
||||
{ dragging: { id: '1' }, setA11yMessage, setDragging },
|
||||
onDrop
|
||||
);
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]')
|
||||
.at(1)
|
||||
.simulate('drop', { preventDefault, stopPropagation });
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'You have dropped the item. You have moved the item from position 1 to positon 3'
|
||||
);
|
||||
expect(preventDefault).toBeCalled();
|
||||
expect(stopPropagation).toBeCalled();
|
||||
expect(onDrop).toBeCalledWith({ id: '1' }, { id: '3' });
|
||||
});
|
||||
|
||||
test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => {
|
||||
const onDrop = jest.fn();
|
||||
const component = mountComponent(
|
||||
{
|
||||
dragging: { id: '1' },
|
||||
activeDropTarget: { activeDropTarget: { id: '3' } } as ActiveDropTarget,
|
||||
keyboardMode: true,
|
||||
},
|
||||
onDrop
|
||||
);
|
||||
const keyboardHandler = component
|
||||
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
|
||||
.simulate('focus');
|
||||
|
||||
act(() => {
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
keyboardHandler.simulate('keydown', { key: 'Enter' });
|
||||
});
|
||||
expect(onDrop).toBeCalledWith({ id: '1' }, { id: '3' });
|
||||
});
|
||||
|
||||
test(`Keyboard Navigation: Reordered elements get extra styles to show the reorder effect`, () => {
|
||||
const setA11yMessage = jest.fn();
|
||||
const component = mountComponent(
|
||||
{ dragging: { id: '1' }, keyboardMode: true, setA11yMessage },
|
||||
jest.fn()
|
||||
);
|
||||
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(+8px)',
|
||||
});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(0).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-40px)',
|
||||
});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
|
||||
).toEqual(undefined);
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'You have moved the item 1 from position 1 to position 2'
|
||||
);
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]')
|
||||
.at(1)
|
||||
.simulate('dragleave');
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
|
||||
).toEqual(undefined);
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
test(`Keyboard Navigation: User cannot move an element outside of the group`, () => {
|
||||
const onDrop = jest.fn();
|
||||
const setActiveDropTarget = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const component = mountComponent(
|
||||
{ dragging: { id: '1' }, keyboardMode: true, setActiveDropTarget, setA11yMessage },
|
||||
onDrop
|
||||
);
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
|
||||
expect(setActiveDropTarget).not.toHaveBeenCalled();
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
|
||||
expect(setActiveDropTarget).toBeCalledWith({ id: '2' });
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'You have moved the item 1 from position 1 to position 2'
|
||||
);
|
||||
});
|
||||
|
||||
test(`Keyboard Navigation: User cannot drop element to itself`, () => {
|
||||
const setActiveDropTarget = jest.fn();
|
||||
const setA11yMessage = jest.fn();
|
||||
const component = mount(
|
||||
<ChildDragDropProvider
|
||||
dragging={dragging}
|
||||
setDragging={() => {
|
||||
dragging = { id: '1' };
|
||||
{...defaultContext}
|
||||
keyboardMode={true}
|
||||
activeDropTarget={{
|
||||
activeDropTarget: { id: '2' },
|
||||
}}
|
||||
dragging={{ id: '1' }}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
setA11yMessage={setA11yMessage}
|
||||
>
|
||||
<ReorderProvider id="groupId">
|
||||
<DragDrop
|
||||
label="1"
|
||||
draggable
|
||||
droppable={false}
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }]}
|
||||
value={{ id: '1' }}
|
||||
>
|
||||
<span>1</span>
|
||||
</DragDrop>
|
||||
<DragDrop
|
||||
label="2"
|
||||
draggable
|
||||
droppable
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
itemsInGroup={['1']}
|
||||
value={{ id: '1' }}
|
||||
onDrop={jest.fn()}
|
||||
dropTo={jest.fn()}
|
||||
reorderableGroup={[{ id: '1' }, { id: '2' }]}
|
||||
value={{ id: '2' }}
|
||||
>
|
||||
<div />
|
||||
<span>2</span>
|
||||
</DragDrop>
|
||||
</ReorderProvider>
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
expect(component.find(ReorderableDragDrop)).toHaveLength(0);
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
|
||||
expect(setActiveDropTarget).toBeCalledWith({ id: '1' });
|
||||
expect(setA11yMessage).toBeCalledWith('You have moved back the item 1 to position 1');
|
||||
});
|
||||
test(`Reorderable component renders properly`, () => {
|
||||
const component = mountComponent(undefined, jest.fn());
|
||||
expect(component.find(ReorderableDragDrop)).toHaveLength(3);
|
||||
});
|
||||
test(`Elements between dragged and drop get extra class to show the reorder effect when dragging`, () => {
|
||||
const component = mountComponent({ id: '1' }, jest.fn());
|
||||
const dataTransfer = {
|
||||
setData: jest.fn(),
|
||||
getData: jest.fn(),
|
||||
};
|
||||
component
|
||||
.find(ReorderableDragDrop)
|
||||
.first()
|
||||
.find('[data-test-subj="lnsDragDrop"]')
|
||||
.simulate('dragstart', { dataTransfer });
|
||||
|
||||
test(`Keyboard Navigation: Doesn't call onDrop when movement is cancelled`, () => {
|
||||
const setA11yMessage = jest.fn();
|
||||
const onDrop = jest.fn();
|
||||
|
||||
const component = mountComponent({ dragging: { id: '1' }, setA11yMessage }, onDrop);
|
||||
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'Escape' });
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrop"]').at(2).simulate('dragover');
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
|
||||
).toEqual({});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(1).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-40px)',
|
||||
});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(2).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-40px)',
|
||||
});
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrop"]').at(2).simulate('dragleave');
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(1).prop('style')
|
||||
).toEqual({});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(2).prop('style')
|
||||
).toEqual({});
|
||||
});
|
||||
test(`Dropping an item runs onDrop function`, () => {
|
||||
const preventDefault = jest.fn();
|
||||
const stopPropagation = jest.fn();
|
||||
const onDrop = jest.fn();
|
||||
|
||||
const component = mountComponent({ id: '1' }, onDrop);
|
||||
|
||||
component
|
||||
.find('[data-test-subj="lnsDragDrop-reorderableDrop"]')
|
||||
.at(1)
|
||||
.simulate('drop', { preventDefault, stopPropagation });
|
||||
expect(preventDefault).toBeCalled();
|
||||
expect(stopPropagation).toBeCalled();
|
||||
expect(onDrop).toBeCalledWith({ id: '1' });
|
||||
});
|
||||
test(`Keyboard navigation: user can reorder an element`, () => {
|
||||
const onDrop = jest.fn();
|
||||
const dropTo = jest.fn();
|
||||
const component = mountComponent({ id: '1' }, onDrop, dropTo);
|
||||
const keyboardHandler = component
|
||||
.find(ReorderableDragDrop)
|
||||
.at(1)
|
||||
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
|
||||
expect(onDrop).not.toHaveBeenCalled();
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'Movement cancelled. The item has returned to its starting position 1'
|
||||
);
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
expect(dropTo).toBeCalledWith('3');
|
||||
keyboardHandler.simulate('blur');
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
|
||||
expect(dropTo).toBeCalledWith('1');
|
||||
});
|
||||
test(`Keyboard Navigation: User cannot move an element outside of the group`, () => {
|
||||
const onDrop = jest.fn();
|
||||
const dropTo = jest.fn();
|
||||
const component = mountComponent({ id: '1' }, onDrop, dropTo);
|
||||
const keyboardHandler = component
|
||||
.find(ReorderableDragDrop)
|
||||
.first()
|
||||
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'Space' });
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
|
||||
expect(dropTo).not.toHaveBeenCalled();
|
||||
|
||||
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
|
||||
expect(dropTo).toBeCalledWith('2');
|
||||
expect(onDrop).not.toHaveBeenCalled();
|
||||
expect(setA11yMessage).toBeCalledWith(
|
||||
'Movement cancelled. The item has returned to its starting position 1'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -9,12 +9,13 @@ import classNames from 'classnames';
|
|||
import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export type Dragging =
|
||||
| (Record<string, unknown> & {
|
||||
id: string;
|
||||
})
|
||||
| undefined;
|
||||
export type DragDropIdentifier = Record<string, unknown> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export interface ActiveDropTarget {
|
||||
activeDropTarget?: DragDropIdentifier;
|
||||
}
|
||||
/**
|
||||
* The shape of the drag / drop context.
|
||||
*/
|
||||
|
@ -22,12 +23,26 @@ export interface DragContextState {
|
|||
/**
|
||||
* The item being dragged or undefined.
|
||||
*/
|
||||
dragging: Dragging;
|
||||
dragging?: DragDropIdentifier;
|
||||
|
||||
/**
|
||||
* keyboard mode
|
||||
*/
|
||||
keyboardMode: boolean;
|
||||
/**
|
||||
* keyboard mode
|
||||
*/
|
||||
setKeyboardMode: (mode: boolean) => void;
|
||||
/**
|
||||
* Set the item being dragged.
|
||||
*/
|
||||
setDragging: (dragging: Dragging) => void;
|
||||
setDragging: (dragging?: DragDropIdentifier) => void;
|
||||
|
||||
activeDropTarget?: ActiveDropTarget;
|
||||
|
||||
setActiveDropTarget: (newTarget?: DragDropIdentifier) => void;
|
||||
|
||||
setA11yMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,28 +53,52 @@ export interface DragContextState {
|
|||
export const DragContext = React.createContext<DragContextState>({
|
||||
dragging: undefined,
|
||||
setDragging: () => {},
|
||||
keyboardMode: false,
|
||||
setKeyboardMode: () => {},
|
||||
activeDropTarget: undefined,
|
||||
setActiveDropTarget: () => {},
|
||||
setA11yMessage: () => {},
|
||||
});
|
||||
|
||||
/**
|
||||
* The argument to DragDropProvider.
|
||||
*/
|
||||
export interface ProviderProps {
|
||||
/**
|
||||
* keyboard mode
|
||||
*/
|
||||
keyboardMode: boolean;
|
||||
/**
|
||||
* keyboard mode
|
||||
*/
|
||||
setKeyboardMode: (mode: boolean) => void;
|
||||
/**
|
||||
* Set the item being dragged.
|
||||
*/
|
||||
/**
|
||||
* The item being dragged. If unspecified, the provider will
|
||||
* behave as if it is the root provider.
|
||||
*/
|
||||
dragging: Dragging;
|
||||
dragging?: DragDropIdentifier;
|
||||
|
||||
/**
|
||||
* Sets the item being dragged. If unspecified, the provider
|
||||
* will behave as if it is the root provider.
|
||||
*/
|
||||
setDragging: (dragging: Dragging) => void;
|
||||
setDragging: (dragging?: DragDropIdentifier) => void;
|
||||
|
||||
activeDropTarget?: {
|
||||
activeDropTarget?: DragDropIdentifier;
|
||||
};
|
||||
|
||||
setActiveDropTarget: (newTarget?: DragDropIdentifier) => void;
|
||||
|
||||
/**
|
||||
* The React children.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
|
||||
setA11yMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,15 +109,60 @@ export interface ProviderProps {
|
|||
* @param props
|
||||
*/
|
||||
export function RootDragDropProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState<{ dragging: Dragging }>({
|
||||
const [draggingState, setDraggingState] = useState<{ dragging?: DragDropIdentifier }>({
|
||||
dragging: undefined,
|
||||
});
|
||||
const setDragging = useMemo(() => (dragging: Dragging) => setState({ dragging }), [setState]);
|
||||
const [keyboardModeState, setKeyboardModeState] = useState(false);
|
||||
const [a11yMessageState, setA11yMessageState] = useState('');
|
||||
const [activeDropTargetState, setActiveDropTargetState] = useState<{
|
||||
activeDropTarget?: DragDropIdentifier;
|
||||
}>({
|
||||
activeDropTarget: undefined,
|
||||
});
|
||||
|
||||
const setDragging = useMemo(
|
||||
() => (dragging?: DragDropIdentifier) => setDraggingState({ dragging }),
|
||||
[setDraggingState]
|
||||
);
|
||||
|
||||
const setA11yMessage = useMemo(() => (message: string) => setA11yMessageState(message), [
|
||||
setA11yMessageState,
|
||||
]);
|
||||
|
||||
const setActiveDropTarget = useMemo(
|
||||
() => (activeDropTarget?: DragDropIdentifier) =>
|
||||
setActiveDropTargetState((s) => ({ ...s, activeDropTarget })),
|
||||
[setActiveDropTargetState]
|
||||
);
|
||||
|
||||
return (
|
||||
<ChildDragDropProvider dragging={state.dragging} setDragging={setDragging}>
|
||||
{children}
|
||||
</ChildDragDropProvider>
|
||||
<div>
|
||||
<ChildDragDropProvider
|
||||
keyboardMode={keyboardModeState}
|
||||
setKeyboardMode={setKeyboardModeState}
|
||||
dragging={draggingState.dragging}
|
||||
setA11yMessage={setA11yMessage}
|
||||
setDragging={setDragging}
|
||||
activeDropTarget={activeDropTargetState}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
>
|
||||
{children}
|
||||
</ChildDragDropProvider>
|
||||
<EuiPortal>
|
||||
<EuiScreenReaderOnly>
|
||||
<div>
|
||||
<p aria-live="assertive" aria-atomic={true}>
|
||||
{a11yMessageState}
|
||||
</p>
|
||||
<p id={`lnsDragDrop-keyboardInstructions`}>
|
||||
{i18n.translate('xpack.lens.dragDrop.keyboardInstructions', {
|
||||
defaultMessage: `Press enter or space to start reordering the dimension group. When dragging, use arrow keys to reorder. Press enter or space again to finish.`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</EuiScreenReaderOnly>
|
||||
</EuiPortal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -89,8 +173,36 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
*
|
||||
* @param props
|
||||
*/
|
||||
export function ChildDragDropProvider({ dragging, setDragging, children }: ProviderProps) {
|
||||
const value = useMemo(() => ({ dragging, setDragging }), [setDragging, dragging]);
|
||||
export function ChildDragDropProvider({
|
||||
dragging,
|
||||
setDragging,
|
||||
setKeyboardMode,
|
||||
keyboardMode,
|
||||
activeDropTarget,
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
children,
|
||||
}: ProviderProps) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
setKeyboardMode,
|
||||
keyboardMode,
|
||||
dragging,
|
||||
setDragging,
|
||||
activeDropTarget,
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
}),
|
||||
[
|
||||
setDragging,
|
||||
dragging,
|
||||
activeDropTarget,
|
||||
setActiveDropTarget,
|
||||
setKeyboardMode,
|
||||
keyboardMode,
|
||||
setA11yMessage,
|
||||
]
|
||||
);
|
||||
return <DragContext.Provider value={value}>{children}</DragContext.Provider>;
|
||||
}
|
||||
|
||||
|
@ -98,7 +210,7 @@ export interface ReorderState {
|
|||
/**
|
||||
* Ids of the elements that are translated up or down
|
||||
*/
|
||||
reorderedItems: string[];
|
||||
reorderedItems: DragDropIdentifier[];
|
||||
|
||||
/**
|
||||
* Direction of the move of dragged element in the reordered list
|
||||
|
@ -112,10 +224,6 @@ export interface ReorderState {
|
|||
* indicates that user is in keyboard mode
|
||||
*/
|
||||
isReorderOn: boolean;
|
||||
/**
|
||||
* aria-live message for changes in reordering
|
||||
*/
|
||||
keyboardReorderMessage: string;
|
||||
/**
|
||||
* reorder group needed for screen reader aria-described-by attribute
|
||||
*/
|
||||
|
@ -135,7 +243,6 @@ export const ReorderContext = React.createContext<ReorderContextState>({
|
|||
direction: '-',
|
||||
draggingHeight: 40,
|
||||
isReorderOn: false,
|
||||
keyboardReorderMessage: '',
|
||||
groupId: '',
|
||||
},
|
||||
setReorderState: () => () => {},
|
||||
|
@ -155,33 +262,70 @@ export function ReorderProvider({
|
|||
direction: '-',
|
||||
draggingHeight: 40,
|
||||
isReorderOn: false,
|
||||
keyboardReorderMessage: '',
|
||||
groupId: id,
|
||||
});
|
||||
|
||||
const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [
|
||||
setState,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={classNames(className, { 'lnsDragDrop-isActiveGroup': state.isReorderOn })}>
|
||||
<div
|
||||
data-test-subj="lnsDragDrop-reorderableGroup"
|
||||
className={classNames(className, {
|
||||
'lnsDragDrop-isActiveGroup': state.isReorderOn && React.Children.count(children) > 1,
|
||||
})}
|
||||
>
|
||||
<ReorderContext.Provider value={{ reorderState: state, setReorderState }}>
|
||||
{children}
|
||||
</ReorderContext.Provider>
|
||||
<EuiPortal>
|
||||
<EuiScreenReaderOnly>
|
||||
<div>
|
||||
<p aria-live="assertive" aria-atomic={true}>
|
||||
{state.keyboardReorderMessage}
|
||||
</p>
|
||||
<p id={`lnsDragDrop-reorderInstructions-${id}`}>
|
||||
{i18n.translate('xpack.lens.dragDrop.reorderInstructions', {
|
||||
defaultMessage: `Press space bar to start a drag. When dragging, use arrow keys to reorder. Press space bar again to finish.`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</EuiScreenReaderOnly>
|
||||
</EuiPortal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const reorderAnnouncements = {
|
||||
moved: (itemLabel: string, position: number, prevPosition: number) => {
|
||||
return prevPosition === position
|
||||
? i18n.translate('xpack.lens.dragDrop.elementMovedBack', {
|
||||
defaultMessage: `You have moved back the item {itemLabel} to position {prevPosition}`,
|
||||
values: {
|
||||
itemLabel,
|
||||
prevPosition,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.dragDrop.elementMoved', {
|
||||
defaultMessage: `You have moved the item {itemLabel} from position {prevPosition} to position {position}`,
|
||||
values: {
|
||||
itemLabel,
|
||||
position,
|
||||
prevPosition,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
lifted: (itemLabel: string, position: number) =>
|
||||
i18n.translate('xpack.lens.dragDrop.elementLifted', {
|
||||
defaultMessage: `You have lifted an item {itemLabel} in position {position}`,
|
||||
values: {
|
||||
itemLabel,
|
||||
position,
|
||||
},
|
||||
}),
|
||||
|
||||
cancelled: (position: number) =>
|
||||
i18n.translate('xpack.lens.dragDrop.abortMessageReorder', {
|
||||
defaultMessage:
|
||||
'Movement cancelled. The item has returned to its starting position {position}',
|
||||
values: {
|
||||
position,
|
||||
},
|
||||
}),
|
||||
dropped: (position: number, prevPosition: number) =>
|
||||
i18n.translate('xpack.lens.dragDrop.dropMessageReorder', {
|
||||
defaultMessage:
|
||||
'You have dropped the item. You have moved the item from position {prevPosition} to positon {position}',
|
||||
values: {
|
||||
position,
|
||||
prevPosition,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ In your child application, place a `ChildDragDropProvider` at the root of that,
|
|||
|
||||
This enables your child application to share the same drag / drop context as the root application.
|
||||
|
||||
## Dragging
|
||||
## DragDropIdentifier
|
||||
|
||||
An item can be both draggable and droppable at the same time, but for simplicity's sake, we'll treat these two cases separately.
|
||||
|
||||
|
@ -88,7 +88,7 @@ The children `DragDrop` components must have props defined as in the example:
|
|||
droppable
|
||||
dragType="reorder"
|
||||
dropType="reorder"
|
||||
itemsInGroup={fields.map((f) => f.id)} // consists ids of all reorderable elements in the group, eg. ['3', '5', '1']
|
||||
reorderableGroup={fields} // consists all reorderable elements in the group, eg. [{id:'3'}, {id:'5'}, {id:'1'}]
|
||||
value={{
|
||||
id: f.id,
|
||||
}}
|
||||
|
|
|
@ -92,6 +92,14 @@ describe('ConfigPanel', () => {
|
|||
mockDatasource = createMockDatasource('ds1');
|
||||
});
|
||||
|
||||
// in what case is this test needed?
|
||||
it('should fail to render layerPanels if the public API is out of date', () => {
|
||||
const props = getDefaultProps();
|
||||
props.framePublicAPI.datasourceLayers = {};
|
||||
const component = mountWithIntl(<LayerPanels {...props} />);
|
||||
expect(component.find(LayerPanel).exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('focus behavior when adding or removing layers', () => {
|
||||
it('should focus the only layer when resetting the layer', () => {
|
||||
const component = mountWithIntl(<LayerPanels {...getDefaultProps()} />);
|
||||
|
|
|
@ -134,37 +134,42 @@ export function LayerPanels(
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const datasourcePublicAPIs = props.framePublicAPI.datasourceLayers;
|
||||
|
||||
return (
|
||||
<EuiForm className="lnsConfigPanel">
|
||||
{layerIds.map((layerId, index) => (
|
||||
<LayerPanel
|
||||
{...props}
|
||||
setLayerRef={setLayerRef}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
index={index}
|
||||
visualizationState={visualizationState}
|
||||
updateVisualization={setVisualizationState}
|
||||
updateDatasource={updateDatasource}
|
||||
updateAll={updateAll}
|
||||
isOnlyLayer={layerIds.length === 1}
|
||||
onRemoveLayer={() => {
|
||||
dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'REMOVE_OR_CLEAR_LAYER',
|
||||
updater: (state) =>
|
||||
removeLayer({
|
||||
activeVisualization,
|
||||
layerId,
|
||||
trackUiEvent,
|
||||
datasourceMap,
|
||||
state,
|
||||
}),
|
||||
});
|
||||
removeLayerRef(layerId);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{layerIds.map((layerId, layerIndex) =>
|
||||
datasourcePublicAPIs[layerId] ? (
|
||||
<LayerPanel
|
||||
{...props}
|
||||
activeVisualization={activeVisualization}
|
||||
setLayerRef={setLayerRef}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
layerIndex={layerIndex}
|
||||
visualizationState={visualizationState}
|
||||
updateVisualization={setVisualizationState}
|
||||
updateDatasource={updateDatasource}
|
||||
updateAll={updateAll}
|
||||
isOnlyLayer={layerIds.length === 1}
|
||||
onRemoveLayer={() => {
|
||||
dispatch({
|
||||
type: 'UPDATE_STATE',
|
||||
subType: 'REMOVE_OR_CLEAR_LAYER',
|
||||
updater: (state) =>
|
||||
removeLayer({
|
||||
activeVisualization,
|
||||
layerId,
|
||||
trackUiEvent,
|
||||
datasourceMap,
|
||||
state,
|
||||
}),
|
||||
});
|
||||
removeLayerRef(layerId);
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
{activeVisualization.appendLayer && visualizationState && (
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiToolTip
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonIcon, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorIndicator } from './color_indicator';
|
||||
import { PaletteIndicator } from './palette_indicator';
|
||||
import { VisualizationDimensionGroupConfig, AccessorConfig } from '../../../types';
|
||||
|
||||
const triggerLinkA11yText = (label: string) =>
|
||||
i18n.translate('xpack.lens.configure.editConfig', {
|
||||
defaultMessage: 'Edit {label} configuration',
|
||||
values: { label },
|
||||
});
|
||||
|
||||
export function DimensionButton({
|
||||
group,
|
||||
children,
|
||||
onClick,
|
||||
onRemoveClick,
|
||||
accessorConfig,
|
||||
label,
|
||||
}: {
|
||||
group: VisualizationDimensionGroupConfig;
|
||||
children: React.ReactElement;
|
||||
onClick: (id: string) => void;
|
||||
onRemoveClick: (id: string) => void;
|
||||
accessorConfig: AccessorConfig;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<EuiLink
|
||||
className="lnsLayerPanel__dimensionLink"
|
||||
data-test-subj="lnsLayerPanel-dimensionLink"
|
||||
onClick={() => onClick(accessorConfig.columnId)}
|
||||
aria-label={triggerLinkA11yText(label)}
|
||||
title={triggerLinkA11yText(label)}
|
||||
>
|
||||
<ColorIndicator accessorConfig={accessorConfig}>{children}</ColorIndicator>
|
||||
</EuiLink>
|
||||
<EuiButtonIcon
|
||||
className="lnsLayerPanel__dimensionRemove"
|
||||
data-test-subj="indexPattern-dimension-remove"
|
||||
iconType="cross"
|
||||
iconSize="s"
|
||||
size="s"
|
||||
color="danger"
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
|
||||
defaultMessage: 'Remove configuration from "{groupLabel}"',
|
||||
values: { groupLabel: group.groupLabel },
|
||||
})}
|
||||
title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
|
||||
defaultMessage: 'Remove configuration from "{groupLabel}"',
|
||||
values: { groupLabel: group.groupLabel },
|
||||
})}
|
||||
onClick={() => onRemoveClick(accessorConfig.columnId)}
|
||||
/>
|
||||
<PaletteIndicator accessorConfig={accessorConfig} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop';
|
||||
import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation } from '../../../types';
|
||||
import { LayerDatasourceDropProps } from './types';
|
||||
|
||||
const isFromTheSameGroup = (el1: DragDropIdentifier, el2?: DragDropIdentifier) =>
|
||||
el2 && isDraggedOperation(el2) && el1.groupId === el2.groupId && el1.columnId !== el2.columnId;
|
||||
|
||||
const isSelf = (el1: DragDropIdentifier, el2?: DragDropIdentifier) =>
|
||||
isDraggedOperation(el2) && el1.columnId === el2.columnId;
|
||||
|
||||
export function DraggableDimensionButton({
|
||||
layerId,
|
||||
label,
|
||||
accessorIndex,
|
||||
groupIndex,
|
||||
layerIndex,
|
||||
columnId,
|
||||
group,
|
||||
onDrop,
|
||||
children,
|
||||
dragDropContext,
|
||||
layerDatasourceDropProps,
|
||||
layerDatasource,
|
||||
}: {
|
||||
dragDropContext: DragContextState;
|
||||
layerId: string;
|
||||
groupIndex: number;
|
||||
layerIndex: number;
|
||||
onDrop: (droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier) => void;
|
||||
group: VisualizationDimensionGroupConfig;
|
||||
label: string;
|
||||
children: React.ReactElement;
|
||||
layerDatasource: Datasource<unknown, unknown>;
|
||||
layerDatasourceDropProps: LayerDatasourceDropProps;
|
||||
accessorIndex: number;
|
||||
columnId: string;
|
||||
}) {
|
||||
const value = useMemo(() => {
|
||||
return {
|
||||
columnId,
|
||||
groupId: group.groupId,
|
||||
layerId,
|
||||
id: columnId,
|
||||
};
|
||||
}, [columnId, group.groupId, layerId]);
|
||||
|
||||
const { dragging } = dragDropContext;
|
||||
|
||||
const isCurrentGroup = group.groupId === dragging?.groupId;
|
||||
const isOperationDragged = isDraggedOperation(dragging);
|
||||
const canHandleDrop =
|
||||
Boolean(dragDropContext.dragging) &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
|
||||
const dragType = isSelf(value, dragging)
|
||||
? 'move'
|
||||
: isOperationDragged && isCurrentGroup
|
||||
? 'reorder'
|
||||
: 'copy';
|
||||
|
||||
const dropType = isOperationDragged ? (!isCurrentGroup ? 'replace' : 'reorder') : 'add';
|
||||
|
||||
const isCompatibleFromOtherGroup = !isCurrentGroup && canHandleDrop;
|
||||
|
||||
const isDroppable = isOperationDragged
|
||||
? dragType === 'reorder'
|
||||
? isFromTheSameGroup(value, dragging)
|
||||
: isCompatibleFromOtherGroup
|
||||
: canHandleDrop;
|
||||
|
||||
const reorderableGroup = useMemo(
|
||||
() =>
|
||||
group.accessors.map((a) => ({
|
||||
columnId: a.columnId,
|
||||
id: a.columnId,
|
||||
groupId: group.groupId,
|
||||
layerId,
|
||||
})),
|
||||
[group, layerId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}>
|
||||
<DragDrop
|
||||
noKeyboardSupportYet={reorderableGroup.length < 2} // to be removed when navigating outside of groups is added
|
||||
draggable
|
||||
dragType={dragType}
|
||||
dropType={dropType}
|
||||
reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined}
|
||||
value={value}
|
||||
label={label}
|
||||
droppable={dragging && isDroppable}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{children}
|
||||
</DragDrop>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop';
|
||||
import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation } from '../../../types';
|
||||
import { LayerDatasourceDropProps } from './types';
|
||||
|
||||
export function EmptyDimensionButton({
|
||||
dragDropContext,
|
||||
group,
|
||||
layerDatasource,
|
||||
layerDatasourceDropProps,
|
||||
layerId,
|
||||
groupIndex,
|
||||
layerIndex,
|
||||
onClick,
|
||||
onDrop,
|
||||
}: {
|
||||
dragDropContext: DragContextState;
|
||||
layerId: string;
|
||||
groupIndex: number;
|
||||
layerIndex: number;
|
||||
onClick: (id: string) => void;
|
||||
onDrop: (droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier) => void;
|
||||
group: VisualizationDimensionGroupConfig;
|
||||
|
||||
layerDatasource: Datasource<unknown, unknown>;
|
||||
layerDatasourceDropProps: LayerDatasourceDropProps;
|
||||
}) {
|
||||
const handleDrop = (droppedItem: DragDropIdentifier) => onDrop(droppedItem, value);
|
||||
|
||||
const value = useMemo(() => {
|
||||
const newId = generateId();
|
||||
return {
|
||||
columnId: newId,
|
||||
groupId: group.groupId,
|
||||
layerId,
|
||||
isNew: true,
|
||||
id: newId,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [group.accessors.length, group.groupId, layerId]);
|
||||
|
||||
return (
|
||||
<div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}>
|
||||
<DragDrop
|
||||
value={value}
|
||||
onDrop={handleDrop}
|
||||
droppable={
|
||||
Boolean(dragDropContext.dragging) &&
|
||||
// Verify that the dragged item is not coming from the same group
|
||||
// since this would be a duplicate
|
||||
(!isDraggedOperation(dragDropContext.dragging) ||
|
||||
dragDropContext.dragging.groupId !== group.groupId) &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: value.columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty">
|
||||
<EuiButtonEmpty
|
||||
className="lnsLayerPanel__triggerText"
|
||||
color="text"
|
||||
size="xs"
|
||||
iconType="plusInCircleFilled"
|
||||
contentProps={{
|
||||
className: 'lnsLayerPanel__triggerTextContent',
|
||||
}}
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnAriaLabel', {
|
||||
defaultMessage: 'Drop a field or click to add to {groupLabel}',
|
||||
values: { groupLabel: group.groupLabel },
|
||||
})}
|
||||
data-test-subj="lns-empty-dimension"
|
||||
onClick={() => {
|
||||
onClick(value.columnId);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.configure.emptyConfig"
|
||||
defaultMessage="Drop a field or click to add"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
</DragDrop>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -76,6 +76,7 @@
|
|||
|
||||
.lnsLayerPanel__dimensionContainer {
|
||||
margin: 0 $euiSizeS $euiSizeS;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
|
@ -127,12 +128,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.lnsLayerPanel__dimensionLink {
|
||||
// Added .lnsLayerPanel__dimension specificity required for animation style override
|
||||
.lnsLayerPanel__dimension .lnsLayerPanel__dimensionLink {
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
background-color: transparent !important; // sass-lint:disable-line no-important
|
||||
outline: none !important; // sass-lint:disable-line no-important
|
||||
background-color: transparent;
|
||||
animation: none !important; // sass-lint:disable-line no-important
|
||||
}
|
||||
|
||||
&:focus .lnsLayerPanel__triggerTextLabel,
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
createMockDatasource,
|
||||
DatasourceMock,
|
||||
} from '../../mocks';
|
||||
import { ChildDragDropProvider, DroppableEvent } from '../../../drag_drop';
|
||||
import { ChildDragDropProvider, DroppableEvent, DragDrop } from '../../../drag_drop';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { Visualization } from '../../../types';
|
||||
|
@ -22,9 +22,20 @@ import { generateId } from '../../../id_generator';
|
|||
|
||||
jest.mock('../../../id_generator');
|
||||
|
||||
const defaultContext = {
|
||||
dragging: undefined,
|
||||
setDragging: jest.fn(),
|
||||
setActiveDropTarget: () => {},
|
||||
activeDropTarget: undefined,
|
||||
keyboardMode: false,
|
||||
setKeyboardMode: () => {},
|
||||
setA11yMessage: jest.fn(),
|
||||
};
|
||||
|
||||
describe('LayerPanel', () => {
|
||||
let mockVisualization: jest.Mocked<Visualization>;
|
||||
let mockVisualization2: jest.Mocked<Visualization>;
|
||||
|
||||
let mockDatasource: DatasourceMock;
|
||||
|
||||
function getDefaultProps() {
|
||||
|
@ -34,11 +45,7 @@ describe('LayerPanel', () => {
|
|||
};
|
||||
return {
|
||||
layerId: 'first',
|
||||
activeVisualizationId: 'vis1',
|
||||
visualizationMap: {
|
||||
vis1: mockVisualization,
|
||||
vis2: mockVisualization2,
|
||||
},
|
||||
activeVisualization: mockVisualization,
|
||||
activeDatasourceId: 'ds1',
|
||||
datasourceMap: {
|
||||
ds1: mockDatasource,
|
||||
|
@ -58,7 +65,7 @@ describe('LayerPanel', () => {
|
|||
onRemoveLayer: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
core: coreMock.createStart(),
|
||||
index: 0,
|
||||
layerIndex: 0,
|
||||
setLayerRef: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
@ -92,20 +99,6 @@ describe('LayerPanel', () => {
|
|||
mockDatasource = createMockDatasource('ds1');
|
||||
});
|
||||
|
||||
it('should fail to render if the public API is out of date', () => {
|
||||
const props = getDefaultProps();
|
||||
props.framePublicAPI.datasourceLayers = {};
|
||||
const component = mountWithIntl(<LayerPanel {...props} />);
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail to render if the active visualization is missing', () => {
|
||||
const component = mountWithIntl(
|
||||
<LayerPanel {...getDefaultProps()} activeVisualizationId="missing" />
|
||||
);
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
describe('layer reset and remove', () => {
|
||||
it('should show the reset button when single layer', () => {
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
|
@ -147,8 +140,7 @@ describe('LayerPanel', () => {
|
|||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
|
||||
const group = component.find('DragDrop[data-test-subj="lnsGroup"]');
|
||||
const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]');
|
||||
expect(group).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
@ -167,8 +159,7 @@ describe('LayerPanel', () => {
|
|||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
|
||||
const group = component.find('DragDrop[data-test-subj="lnsGroup"]');
|
||||
const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]');
|
||||
expect(group).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
@ -231,50 +222,6 @@ describe('LayerPanel', () => {
|
|||
expect(panel.props.children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should keep the DimensionContainer open when configuring a new dimension', () => {
|
||||
/**
|
||||
* The ID generation system for new dimensions has been messy before, so
|
||||
* this tests that the ID used in the first render is used to keep the container
|
||||
* open in future renders
|
||||
*/
|
||||
(generateId as jest.Mock).mockReturnValueOnce(`newid`);
|
||||
(generateId as jest.Mock).mockReturnValueOnce(`bad`);
|
||||
mockVisualization.getConfiguration.mockReturnValueOnce({
|
||||
groups: [
|
||||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: [],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
],
|
||||
});
|
||||
// Normally the configuration would change in response to a state update,
|
||||
// but this test is updating it directly
|
||||
mockVisualization.getConfiguration.mockReturnValueOnce({
|
||||
groups: [
|
||||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: [{ columnId: 'newid' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: false,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
});
|
||||
component.update();
|
||||
|
||||
expect(component.find('EuiFlyoutHeader').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not update the visualization if the datasource is incomplete', () => {
|
||||
(generateId as jest.Mock).mockReturnValueOnce(`newid`);
|
||||
const updateAll = jest.fn();
|
||||
|
@ -338,13 +285,12 @@ describe('LayerPanel', () => {
|
|||
expect(updateAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close the DimensionContainer when the active visualization changes', () => {
|
||||
it('should keep the DimensionContainer open when configuring a new dimension', () => {
|
||||
/**
|
||||
* The ID generation system for new dimensions has been messy before, so
|
||||
* this tests that the ID used in the first render is used to keep the container
|
||||
* open in future renders
|
||||
*/
|
||||
|
||||
(generateId as jest.Mock).mockReturnValueOnce(`newid`);
|
||||
(generateId as jest.Mock).mockReturnValueOnce(`bad`);
|
||||
mockVisualization.getConfiguration.mockReturnValueOnce({
|
||||
|
@ -374,6 +320,51 @@ describe('LayerPanel', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
act(() => {
|
||||
component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click');
|
||||
});
|
||||
component.update();
|
||||
|
||||
expect(component.find('EuiFlyoutHeader').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should close the DimensionContainer when the active visualization changes', () => {
|
||||
/**
|
||||
* The ID generation system for new dimensions has been messy before, so
|
||||
* this tests that the ID used in the first render is used to keep the container
|
||||
* open in future renders
|
||||
*/
|
||||
|
||||
(generateId as jest.Mock).mockReturnValueOnce(`newid`);
|
||||
(generateId as jest.Mock).mockReturnValueOnce(`bad`);
|
||||
mockVisualization.getConfiguration.mockReturnValueOnce({
|
||||
groups: [
|
||||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: [],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
],
|
||||
});
|
||||
// Normally the configuration would change in response to a state update,
|
||||
// but this test is updating it directly
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: [{ columnId: 'newid' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: false,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
|
||||
|
||||
act(() => {
|
||||
|
@ -382,7 +373,7 @@ describe('LayerPanel', () => {
|
|||
component.update();
|
||||
expect(component.find('EuiFlyoutHeader').exists()).toBe(true);
|
||||
act(() => {
|
||||
component.setProps({ activeVisualizationId: 'vis2' });
|
||||
component.setProps({ activeVisualization: mockVisualization2 });
|
||||
});
|
||||
component.update();
|
||||
expect(component.find('EuiFlyoutHeader').exists()).toBe(false);
|
||||
|
@ -452,7 +443,7 @@ describe('LayerPanel', () => {
|
|||
const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' };
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider dragging={draggingField} setDragging={jest.fn()}>
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingField}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
|
@ -465,7 +456,7 @@ describe('LayerPanel', () => {
|
|||
})
|
||||
);
|
||||
|
||||
component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop');
|
||||
component.find('[data-test-subj="lnsGroup"] DragDrop').first().simulate('drop');
|
||||
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -495,7 +486,7 @@ describe('LayerPanel', () => {
|
|||
const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' };
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider dragging={draggingField} setDragging={jest.fn()}>
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingField}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
|
@ -505,10 +496,14 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
|
||||
expect(
|
||||
component.find('DragDrop[data-test-subj="lnsGroup"]').first().prop('droppable')
|
||||
component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('droppable')
|
||||
).toEqual(false);
|
||||
|
||||
component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop');
|
||||
component
|
||||
.find('[data-test-subj="lnsGroup"] DragDrop')
|
||||
.first()
|
||||
.find('.lnsLayerPanel__dimension')
|
||||
.simulate('drop');
|
||||
|
||||
expect(mockDatasource.onDrop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -542,12 +537,11 @@ describe('LayerPanel', () => {
|
|||
const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' };
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider dragging={draggingOperation} setDragging={jest.fn()}>
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
expect(mockDatasource.canHandleDrop).toHaveBeenCalledTimes(2);
|
||||
expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dragDropContext: expect.objectContaining({
|
||||
|
@ -557,7 +551,7 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
|
||||
// Simulate drop on the pre-populated dimension
|
||||
component.find('DragDrop[data-test-subj="lnsGroupB"]').at(0).simulate('drop');
|
||||
component.find('[data-test-subj="lnsGroupB"] DragDrop').at(0).simulate('drop');
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: 'b',
|
||||
|
@ -568,7 +562,7 @@ describe('LayerPanel', () => {
|
|||
);
|
||||
|
||||
// Simulate drop on the empty dimension
|
||||
component.find('DragDrop[data-test-subj="lnsGroupB"]').at(1).simulate('drop');
|
||||
component.find('[data-test-subj="lnsGroupB"] DragDrop').at(1).simulate('drop');
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: 'newid',
|
||||
|
@ -596,18 +590,55 @@ describe('LayerPanel', () => {
|
|||
const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' };
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider dragging={draggingOperation} setDragging={jest.fn()}>
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
expect(mockDatasource.canHandleDrop).not.toHaveBeenCalled();
|
||||
component.find('DragDrop[data-test-subj="lnsGroup"]').at(1).prop('onDrop')!(
|
||||
component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, {
|
||||
layerId: 'first',
|
||||
columnId: 'b',
|
||||
groupId: 'a',
|
||||
id: 'b',
|
||||
});
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
groupId: 'a',
|
||||
droppedItem: draggingOperation,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should copy when dropping on empty slot in the same group', () => {
|
||||
mockVisualization.getConfiguration.mockReturnValue({
|
||||
groups: [
|
||||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: [{ columnId: 'a' }, { columnId: 'b' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' };
|
||||
|
||||
const component = mountWithIntl(
|
||||
<ChildDragDropProvider {...defaultContext} dragging={draggingOperation}>
|
||||
<LayerPanel {...getDefaultProps()} />
|
||||
</ChildDragDropProvider>
|
||||
);
|
||||
|
||||
component.find('[data-test-subj="lnsGroup"] DragDrop').at(2).prop('onDrop')!(
|
||||
(draggingOperation as unknown) as DroppableEvent
|
||||
);
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isReorder: true,
|
||||
groupId: 'a',
|
||||
droppedItem: draggingOperation,
|
||||
isNew: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -5,66 +5,35 @@
|
|||
*/
|
||||
import './layer_panel.scss';
|
||||
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import React, { useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import { StateSetter, isDraggedOperation } from '../../../types';
|
||||
import { DragContext, DragDrop, ChildDragDropProvider, ReorderProvider } from '../../../drag_drop';
|
||||
import { StateSetter, Visualization } from '../../../types';
|
||||
import {
|
||||
DragContext,
|
||||
DragDropIdentifier,
|
||||
ChildDragDropProvider,
|
||||
ReorderProvider,
|
||||
} from '../../../drag_drop';
|
||||
import { LayerSettings } from './layer_settings';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { ConfigPanelWrapperProps, ActiveDimensionState } from './types';
|
||||
import { LayerPanelProps, ActiveDimensionState } from './types';
|
||||
import { DimensionContainer } from './dimension_container';
|
||||
import { ColorIndicator } from './color_indicator';
|
||||
import { PaletteIndicator } from './palette_indicator';
|
||||
|
||||
const triggerLinkA11yText = (label: string) =>
|
||||
i18n.translate('xpack.lens.configure.editConfig', {
|
||||
defaultMessage: 'Click to edit configuration for {label} or drag to move',
|
||||
values: { label },
|
||||
});
|
||||
import { RemoveLayerButton } from './remove_layer_button';
|
||||
import { EmptyDimensionButton } from './empty_dimension_button';
|
||||
import { DimensionButton } from './dimension_button';
|
||||
import { DraggableDimensionButton } from './draggable_dimension_button';
|
||||
|
||||
const initialActiveDimensionState = {
|
||||
isNew: false,
|
||||
};
|
||||
|
||||
function isConfiguration(
|
||||
value: unknown
|
||||
): value is { columnId: string; groupId: string; layerId: string } {
|
||||
return (
|
||||
Boolean(value) &&
|
||||
typeof value === 'object' &&
|
||||
'columnId' in value! &&
|
||||
'groupId' in value &&
|
||||
'layerId' in value
|
||||
);
|
||||
}
|
||||
|
||||
function isSameConfiguration(config1: unknown, config2: unknown) {
|
||||
return (
|
||||
isConfiguration(config1) &&
|
||||
isConfiguration(config2) &&
|
||||
config1.columnId === config2.columnId &&
|
||||
config1.groupId === config2.groupId &&
|
||||
config1.layerId === config2.layerId
|
||||
);
|
||||
}
|
||||
|
||||
export function LayerPanel(
|
||||
props: Exclude<ConfigPanelWrapperProps, 'state' | 'setState'> & {
|
||||
props: Exclude<LayerPanelProps, 'state' | 'setState'> & {
|
||||
activeVisualization: Visualization;
|
||||
layerId: string;
|
||||
index: number;
|
||||
layerIndex: number;
|
||||
isOnlyLayer: boolean;
|
||||
updateVisualization: StateSetter<unknown>;
|
||||
updateDatasource: (datasourceId: string, newState: unknown) => void;
|
||||
|
@ -82,26 +51,25 @@ export function LayerPanel(
|
|||
initialActiveDimensionState
|
||||
);
|
||||
|
||||
const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer, setLayerRef, index } = props;
|
||||
const {
|
||||
framePublicAPI,
|
||||
layerId,
|
||||
isOnlyLayer,
|
||||
onRemoveLayer,
|
||||
setLayerRef,
|
||||
layerIndex,
|
||||
activeVisualization,
|
||||
updateVisualization,
|
||||
updateDatasource,
|
||||
} = props;
|
||||
const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId];
|
||||
|
||||
useEffect(() => {
|
||||
setActiveDimension(initialActiveDimensionState);
|
||||
}, [props.activeVisualizationId]);
|
||||
}, [activeVisualization.id]);
|
||||
|
||||
const setLayerRefMemoized = React.useCallback((el) => setLayerRef(layerId, el), [
|
||||
layerId,
|
||||
setLayerRef,
|
||||
]);
|
||||
const setLayerRefMemoized = useCallback((el) => setLayerRef(layerId, el), [layerId, setLayerRef]);
|
||||
|
||||
if (
|
||||
!datasourcePublicAPI ||
|
||||
!props.activeVisualizationId ||
|
||||
!props.visualizationMap[props.activeVisualizationId]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const activeVisualization = props.visualizationMap[props.activeVisualizationId];
|
||||
const layerVisualizationConfigProps = {
|
||||
layerId,
|
||||
dragDropContext,
|
||||
|
@ -110,18 +78,23 @@ export function LayerPanel(
|
|||
dateRange: props.framePublicAPI.dateRange,
|
||||
activeData: props.framePublicAPI.activeData,
|
||||
};
|
||||
|
||||
const datasourceId = datasourcePublicAPI.datasourceId;
|
||||
const layerDatasourceState = props.datasourceStates[datasourceId].state;
|
||||
const layerDatasource = props.datasourceMap[datasourceId];
|
||||
|
||||
const layerDatasourceDropProps = {
|
||||
layerId,
|
||||
dragDropContext,
|
||||
state: layerDatasourceState,
|
||||
setState: (newState: unknown) => {
|
||||
props.updateDatasource(datasourceId, newState);
|
||||
},
|
||||
};
|
||||
const layerDatasourceDropProps = useMemo(
|
||||
() => ({
|
||||
layerId,
|
||||
dragDropContext,
|
||||
state: layerDatasourceState,
|
||||
setState: (newState: unknown) => {
|
||||
updateDatasource(datasourceId, newState);
|
||||
},
|
||||
}),
|
||||
[layerId, dragDropContext, layerDatasourceState, datasourceId, updateDatasource]
|
||||
);
|
||||
|
||||
const layerDatasource = props.datasourceMap[datasourceId];
|
||||
|
||||
const layerDatasourceConfigProps = {
|
||||
...layerDatasourceDropProps,
|
||||
|
@ -135,10 +108,68 @@ export function LayerPanel(
|
|||
const { activeId, activeGroup } = activeDimension;
|
||||
|
||||
const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state);
|
||||
|
||||
const { setDimension, removeDimension } = activeVisualization;
|
||||
const layerDatasourceOnDrop = layerDatasource.onDrop;
|
||||
|
||||
const onDrop = useMemo(() => {
|
||||
return (droppedItem: DragDropIdentifier, targetItem: DragDropIdentifier) => {
|
||||
const { columnId, groupId, layerId: targetLayerId, isNew } = (targetItem as unknown) as {
|
||||
groupId: string;
|
||||
columnId: string;
|
||||
layerId: string;
|
||||
isNew?: boolean;
|
||||
};
|
||||
|
||||
const filterOperations =
|
||||
groups.find(({ groupId: gId }) => gId === targetItem.groupId)?.filterOperations ||
|
||||
(() => false);
|
||||
|
||||
const dropResult = layerDatasourceOnDrop({
|
||||
...layerDatasourceDropProps,
|
||||
droppedItem,
|
||||
columnId,
|
||||
groupId,
|
||||
layerId: targetLayerId,
|
||||
isNew,
|
||||
filterOperations,
|
||||
});
|
||||
if (dropResult) {
|
||||
updateVisualization(
|
||||
setDimension({
|
||||
columnId,
|
||||
groupId,
|
||||
layerId: targetLayerId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
|
||||
if (typeof dropResult === 'object') {
|
||||
// When a column is moved, we delete the reference to the old
|
||||
updateVisualization(
|
||||
removeDimension({
|
||||
columnId: dropResult.deleted,
|
||||
layerId: targetLayerId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [
|
||||
groups,
|
||||
layerDatasourceOnDrop,
|
||||
props.visualizationState,
|
||||
updateVisualization,
|
||||
setDimension,
|
||||
removeDimension,
|
||||
layerDatasourceDropProps,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ChildDragDropProvider {...dragDropContext}>
|
||||
<section tabIndex={-1} ref={setLayerRefMemoized} className="lnsLayerPanel">
|
||||
<EuiPanel data-test-subj={`lns-layerPanel-${index}`} paddingSize="s">
|
||||
<EuiPanel data-test-subj={`lns-layerPanel-${layerIndex}`} paddingSize="s">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
|
||||
<EuiFlexItem grow={false} className="lnsLayerPanel__settingsFlexItem">
|
||||
<LayerSettings
|
||||
|
@ -193,10 +224,8 @@ export function LayerPanel(
|
|||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{groups.map((group) => {
|
||||
const newId = generateId();
|
||||
{groups.map((group, groupIndex) => {
|
||||
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
className={
|
||||
|
@ -222,261 +251,86 @@ export function LayerPanel(
|
|||
}
|
||||
>
|
||||
<>
|
||||
<ReorderProvider
|
||||
id={`${layerId}-${group.groupId}`}
|
||||
className={'lnsLayerPanel__group'}
|
||||
>
|
||||
{group.accessors.map((accessorConfig) => {
|
||||
const accessor = accessorConfig.columnId;
|
||||
const { dragging } = dragDropContext;
|
||||
const dragType =
|
||||
isDraggedOperation(dragging) && accessor === dragging.columnId
|
||||
? 'move'
|
||||
: isDraggedOperation(dragging) && group.groupId === dragging.groupId
|
||||
? 'reorder'
|
||||
: 'copy';
|
||||
|
||||
const dropType = isDraggedOperation(dragging)
|
||||
? group.groupId !== dragging.groupId
|
||||
? 'replace'
|
||||
: 'reorder'
|
||||
: 'add';
|
||||
|
||||
const isFromCompatibleGroup =
|
||||
dragging?.groupId !== group.groupId &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
|
||||
const isFromTheSameGroup =
|
||||
isDraggedOperation(dragging) &&
|
||||
dragging.groupId === group.groupId &&
|
||||
dragging.columnId !== accessor;
|
||||
|
||||
const isDroppable = isDraggedOperation(dragging)
|
||||
? dragType === 'reorder'
|
||||
? isFromTheSameGroup
|
||||
: isFromCompatibleGroup
|
||||
: layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
<ReorderProvider id={group.groupId} className={'lnsLayerPanel__group'}>
|
||||
{group.accessors.map((accessorConfig, accessorIndex) => {
|
||||
const { columnId } = accessorConfig;
|
||||
|
||||
return (
|
||||
<DragDrop
|
||||
key={accessor}
|
||||
draggable={!activeId}
|
||||
dragType={dragType}
|
||||
dropType={dropType}
|
||||
data-test-subj={group.dataTestSubj}
|
||||
itemsInGroup={group.accessors.map((a) =>
|
||||
typeof a === 'string' ? a : a.columnId
|
||||
)}
|
||||
className={'lnsLayerPanel__dimensionContainer'}
|
||||
value={{
|
||||
columnId: accessor,
|
||||
groupId: group.groupId,
|
||||
layerId,
|
||||
id: accessor,
|
||||
}}
|
||||
isValueEqual={isSameConfiguration}
|
||||
label={columnLabelMap[accessor]}
|
||||
droppable={dragging && isDroppable}
|
||||
dropTo={(dropTargetId: string) => {
|
||||
layerDatasource.onDrop({
|
||||
isReorder: true,
|
||||
...layerDatasourceDropProps,
|
||||
droppedItem: {
|
||||
columnId: accessor,
|
||||
groupId: group.groupId,
|
||||
layerId,
|
||||
id: accessor,
|
||||
},
|
||||
columnId: dropTargetId,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
}}
|
||||
onDrop={(droppedItem) => {
|
||||
const isReorder =
|
||||
isDraggedOperation(droppedItem) &&
|
||||
droppedItem.groupId === group.groupId &&
|
||||
droppedItem.columnId !== accessor;
|
||||
|
||||
const dropResult = layerDatasource.onDrop({
|
||||
isReorder,
|
||||
...layerDatasourceDropProps,
|
||||
droppedItem,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
if (typeof dropResult === 'object') {
|
||||
// When a column is moved, we delete the reference to the old
|
||||
props.updateVisualization(
|
||||
activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: dropResult.deleted,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
<DraggableDimensionButton
|
||||
accessorIndex={accessorIndex}
|
||||
columnId={columnId}
|
||||
dragDropContext={dragDropContext}
|
||||
group={group}
|
||||
groupIndex={groupIndex}
|
||||
key={columnId}
|
||||
layerDatasourceDropProps={layerDatasourceDropProps}
|
||||
label={columnLabelMap[columnId]}
|
||||
layerDatasource={layerDatasource}
|
||||
layerIndex={layerIndex}
|
||||
layerId={layerId}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<div className="lnsLayerPanel__dimension">
|
||||
<EuiLink
|
||||
className="lnsLayerPanel__dimensionLink"
|
||||
data-test-subj="lnsLayerPanel-dimensionLink"
|
||||
onClick={() => {
|
||||
if (activeId) {
|
||||
setActiveDimension(initialActiveDimensionState);
|
||||
} else {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: accessor,
|
||||
});
|
||||
}
|
||||
<DimensionButton
|
||||
accessorConfig={accessorConfig}
|
||||
label={columnLabelMap[accessorConfig.columnId]}
|
||||
group={group}
|
||||
onClick={(id: string) => {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
});
|
||||
}}
|
||||
aria-label={triggerLinkA11yText(columnLabelMap[accessor])}
|
||||
title={triggerLinkA11yText(columnLabelMap[accessor])}
|
||||
>
|
||||
<ColorIndicator accessorConfig={accessorConfig}>
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
}}
|
||||
/>
|
||||
</ColorIndicator>
|
||||
</EuiLink>
|
||||
<EuiButtonIcon
|
||||
className="lnsLayerPanel__dimensionRemove"
|
||||
data-test-subj="indexPattern-dimension-remove"
|
||||
iconType="cross"
|
||||
iconSize="s"
|
||||
size="s"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.indexPattern.removeColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Remove configuration from "{groupLabel}"',
|
||||
values: { groupLabel: group.groupLabel },
|
||||
}
|
||||
)}
|
||||
title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', {
|
||||
defaultMessage: 'Remove configuration from "{groupLabel}"',
|
||||
values: { groupLabel: group.groupLabel },
|
||||
})}
|
||||
onClick={() => {
|
||||
onRemoveClick={(id: string) => {
|
||||
trackUiEvent('indexpattern_dimension_removed');
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
layerDatasource.removeColumn({
|
||||
layerId,
|
||||
columnId: accessor,
|
||||
columnId: id,
|
||||
prevState: layerDatasourceState,
|
||||
}),
|
||||
activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: accessor,
|
||||
columnId: id,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<PaletteIndicator accessorConfig={accessorConfig} />
|
||||
>
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessorConfig.columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
}}
|
||||
/>
|
||||
</DimensionButton>
|
||||
</div>
|
||||
</DragDrop>
|
||||
</DraggableDimensionButton>
|
||||
);
|
||||
})}
|
||||
</ReorderProvider>
|
||||
{group.supportsMoreColumns ? (
|
||||
<div className={'lnsLayerPanel__dimensionContainer'}>
|
||||
<DragDrop
|
||||
data-test-subj={group.dataTestSubj}
|
||||
droppable={
|
||||
Boolean(dragDropContext.dragging) &&
|
||||
// Verify that the dragged item is not coming from the same group
|
||||
// since this would be a reorder
|
||||
(!isDraggedOperation(dragDropContext.dragging) ||
|
||||
dragDropContext.dragging.groupId !== group.groupId) &&
|
||||
layerDatasource.canHandleDrop({
|
||||
...layerDatasourceDropProps,
|
||||
columnId: newId,
|
||||
filterOperations: group.filterOperations,
|
||||
})
|
||||
}
|
||||
onDrop={(droppedItem) => {
|
||||
const dropResult = layerDatasource.onDrop({
|
||||
...layerDatasourceDropProps,
|
||||
droppedItem,
|
||||
columnId: newId,
|
||||
filterOperations: group.filterOperations,
|
||||
});
|
||||
if (dropResult) {
|
||||
props.updateVisualization(
|
||||
activeVisualization.setDimension({
|
||||
layerId,
|
||||
groupId: group.groupId,
|
||||
columnId: newId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
|
||||
if (typeof dropResult === 'object') {
|
||||
// When a column is moved, we delete the reference to the old
|
||||
props.updateVisualization(
|
||||
activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: dropResult.deleted,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty">
|
||||
<EuiButtonEmpty
|
||||
className="lnsLayerPanel__triggerText"
|
||||
color="text"
|
||||
size="xs"
|
||||
iconType="plusInCircleFilled"
|
||||
contentProps={{
|
||||
className: 'lnsLayerPanel__triggerTextContent',
|
||||
}}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.indexPattern.removeColumnAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Drop a field or click to add to {groupLabel}',
|
||||
values: { groupLabel: group.groupLabel },
|
||||
}
|
||||
)}
|
||||
data-test-subj="lns-empty-dimension"
|
||||
onClick={() => {
|
||||
if (activeId) {
|
||||
setActiveDimension(initialActiveDimensionState);
|
||||
} else {
|
||||
setActiveDimension({
|
||||
isNew: true,
|
||||
activeGroup: group,
|
||||
activeId: newId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.configure.emptyConfig"
|
||||
defaultMessage="Drop a field or click to add"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
</DragDrop>
|
||||
</div>
|
||||
<EmptyDimensionButton
|
||||
dragDropContext={dragDropContext}
|
||||
group={group}
|
||||
groupIndex={groupIndex}
|
||||
layerId={layerId}
|
||||
layerIndex={layerIndex}
|
||||
layerDatasource={layerDatasource}
|
||||
layerDatasourceDropProps={layerDatasourceDropProps}
|
||||
onClick={(id) => {
|
||||
setActiveDimension({
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
isNew: true,
|
||||
});
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
|
@ -572,44 +426,11 @@ export function LayerPanel(
|
|||
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="lnsLayerRemove"
|
||||
aria-label={
|
||||
isOnlyLayer
|
||||
? i18n.translate('xpack.lens.resetLayerAriaLabel', {
|
||||
defaultMessage: 'Reset layer {index}',
|
||||
values: { index: index + 1 },
|
||||
})
|
||||
: i18n.translate('xpack.lens.deleteLayerAriaLabel', {
|
||||
defaultMessage: `Delete layer {index}`,
|
||||
values: { index: index + 1 },
|
||||
})
|
||||
}
|
||||
onClick={() => {
|
||||
// If we don't blur the remove / clear button, it remains focused
|
||||
// which is a strange UX in this case. e.target.blur doesn't work
|
||||
// due to who knows what, but probably event re-writing. Additionally,
|
||||
// activeElement does not have blur so, we need to do some casting + safeguards.
|
||||
const el = (document.activeElement as unknown) as { blur: () => void };
|
||||
|
||||
if (el?.blur) {
|
||||
el.blur();
|
||||
}
|
||||
|
||||
onRemoveLayer();
|
||||
}}
|
||||
>
|
||||
{isOnlyLayer
|
||||
? i18n.translate('xpack.lens.resetLayer', {
|
||||
defaultMessage: 'Reset layer',
|
||||
})
|
||||
: i18n.translate('xpack.lens.deleteLayer', {
|
||||
defaultMessage: `Delete layer`,
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<RemoveLayerButton
|
||||
onRemoveLayer={onRemoveLayer}
|
||||
layerIndex={layerIndex}
|
||||
isOnlyLayer={isOnlyLayer}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function RemoveLayerButton({
|
||||
onRemoveLayer,
|
||||
layerIndex,
|
||||
isOnlyLayer,
|
||||
}: {
|
||||
onRemoveLayer: () => void;
|
||||
layerIndex: number;
|
||||
isOnlyLayer: boolean;
|
||||
}) {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="lnsLayerRemove"
|
||||
aria-label={
|
||||
isOnlyLayer
|
||||
? i18n.translate('xpack.lens.resetLayerAriaLabel', {
|
||||
defaultMessage: 'Reset layer {index}',
|
||||
values: { index: layerIndex + 1 },
|
||||
})
|
||||
: i18n.translate('xpack.lens.deleteLayerAriaLabel', {
|
||||
defaultMessage: `Delete layer {index}`,
|
||||
values: { index: layerIndex + 1 },
|
||||
})
|
||||
}
|
||||
onClick={() => {
|
||||
// If we don't blur the remove / clear button, it remains focused
|
||||
// which is a strange UX in this case. e.target.blur doesn't work
|
||||
// due to who knows what, but probably event re-writing. Additionally,
|
||||
// activeElement does not have blur so, we need to do some casting + safeguards.
|
||||
const el = (document.activeElement as unknown) as { blur: () => void };
|
||||
|
||||
if (el?.blur) {
|
||||
el.blur();
|
||||
}
|
||||
|
||||
onRemoveLayer();
|
||||
}}
|
||||
>
|
||||
{isOnlyLayer
|
||||
? i18n.translate('xpack.lens.resetLayer', {
|
||||
defaultMessage: 'Reset layer',
|
||||
})
|
||||
: i18n.translate('xpack.lens.deleteLayer', {
|
||||
defaultMessage: `Delete layer`,
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
|
@ -12,7 +12,7 @@ import {
|
|||
DatasourceDimensionEditorProps,
|
||||
VisualizationDimensionGroupConfig,
|
||||
} from '../../../types';
|
||||
|
||||
import { DragContextState } from '../../../drag_drop';
|
||||
export interface ConfigPanelWrapperProps {
|
||||
activeDatasourceId: string;
|
||||
visualizationState: unknown;
|
||||
|
@ -31,6 +31,30 @@ export interface ConfigPanelWrapperProps {
|
|||
core: DatasourceDimensionEditorProps['core'];
|
||||
}
|
||||
|
||||
export interface LayerPanelProps {
|
||||
activeDatasourceId: string;
|
||||
visualizationState: unknown;
|
||||
datasourceMap: Record<string, Datasource>;
|
||||
activeVisualization: Visualization;
|
||||
dispatch: (action: Action) => void;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
datasourceStates: Record<
|
||||
string,
|
||||
{
|
||||
isLoading: boolean;
|
||||
state: unknown;
|
||||
}
|
||||
>;
|
||||
core: DatasourceDimensionEditorProps['core'];
|
||||
}
|
||||
|
||||
export interface LayerDatasourceDropProps {
|
||||
layerId: string;
|
||||
dragDropContext: DragContextState;
|
||||
state: unknown;
|
||||
setState: (newState: unknown) => void;
|
||||
}
|
||||
|
||||
export interface ActiveDimensionState {
|
||||
isNew: boolean;
|
||||
activeId?: string;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
|
||||
import { NativeRenderer } from '../../native_renderer';
|
||||
import { Action } from './state_management';
|
||||
import { DragContext, Dragging } from '../../drag_drop';
|
||||
import { DragContext, DragDropIdentifier } from '../../drag_drop';
|
||||
import { StateSetter, FramePublicAPI, DatasourceDataPanelProps, Datasource } from '../../types';
|
||||
import { Query, Filter } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
|
@ -26,8 +26,8 @@ interface DataPanelWrapperProps {
|
|||
query: Query;
|
||||
dateRange: FramePublicAPI['dateRange'];
|
||||
filters: Filter[];
|
||||
dropOntoWorkspace: (field: Dragging) => void;
|
||||
hasSuggestionForField: (field: Dragging) => boolean;
|
||||
dropOntoWorkspace: (field: DragDropIdentifier) => void;
|
||||
hasSuggestionForField: (field: DragDropIdentifier) => boolean;
|
||||
}
|
||||
|
||||
export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
||||
|
|
|
@ -1338,10 +1338,14 @@ describe('editor_frame', () => {
|
|||
instance.update();
|
||||
|
||||
act(() => {
|
||||
instance.find(DragDrop).filter('[data-test-subj="mockVisA"]').prop('onDrop')!({
|
||||
indexPatternId: '1',
|
||||
field: {},
|
||||
});
|
||||
instance.find('[data-test-subj="mockVisA"]').find(DragDrop).prop('onDrop')!(
|
||||
{
|
||||
indexPatternId: '1',
|
||||
field: {},
|
||||
id: '1',
|
||||
},
|
||||
{ id: 'lnsWorkspace' }
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith(
|
||||
|
@ -1435,10 +1439,14 @@ describe('editor_frame', () => {
|
|||
instance.update();
|
||||
|
||||
act(() => {
|
||||
instance.find(DragDrop).filter('[data-test-subj="lnsWorkspace"]').prop('onDrop')!({
|
||||
indexPatternId: '1',
|
||||
field: {},
|
||||
});
|
||||
instance.find(DragDrop).filter('[dataTestSubj="lnsWorkspace"]').prop('onDrop')!(
|
||||
{
|
||||
indexPatternId: '1',
|
||||
field: {},
|
||||
id: '1',
|
||||
},
|
||||
{ id: 'lnsWorkspace' }
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockVisualization3.getConfiguration).toHaveBeenCalledWith(
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useReducer, useState } from 'react';
|
||||
import React, { useEffect, useReducer, useState, useCallback } from 'react';
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
|
||||
|
@ -16,7 +16,7 @@ import { FrameLayout } from './frame_layout';
|
|||
import { SuggestionPanel } from './suggestion_panel';
|
||||
import { WorkspacePanel } from './workspace_panel';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
import { Dragging, RootDragDropProvider } from '../../drag_drop';
|
||||
import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop';
|
||||
import { getSavedObjectFormat } from './save';
|
||||
import { generateId } from '../../id_generator';
|
||||
import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public';
|
||||
|
@ -260,7 +260,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
);
|
||||
|
||||
const getSuggestionForField = React.useCallback(
|
||||
(field: Dragging) => {
|
||||
(field: DragDropIdentifier) => {
|
||||
const { activeDatasourceId, datasourceStates } = state;
|
||||
const activeVisualizationId = state.visualization.activeId;
|
||||
const visualizationState = state.visualization.state;
|
||||
|
@ -290,12 +290,12 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
]
|
||||
);
|
||||
|
||||
const hasSuggestionForField = React.useCallback(
|
||||
(field: Dragging) => getSuggestionForField(field) !== undefined,
|
||||
const hasSuggestionForField = useCallback(
|
||||
(field: DragDropIdentifier) => getSuggestionForField(field) !== undefined,
|
||||
[getSuggestionForField]
|
||||
);
|
||||
|
||||
const dropOntoWorkspace = React.useCallback(
|
||||
const dropOntoWorkspace = useCallback(
|
||||
(field) => {
|
||||
const suggestion = getSuggestionForField(field);
|
||||
if (suggestion) {
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
DatasourcePublicAPI,
|
||||
} from '../../types';
|
||||
import { Action } from './state_management';
|
||||
import { Dragging } from '../../drag_drop';
|
||||
import { DragDropIdentifier } from '../../drag_drop';
|
||||
|
||||
export interface Suggestion {
|
||||
visualizationId: string;
|
||||
|
@ -231,7 +231,7 @@ export function getTopSuggestionForField(
|
|||
visualizationState: unknown,
|
||||
datasource: Datasource,
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>,
|
||||
field: Dragging
|
||||
field: DragDropIdentifier
|
||||
) {
|
||||
const hasData = Object.values(datasourceLayers).some(
|
||||
(datasourceLayer) => datasourceLayer.getTableSpec().length > 0
|
||||
|
|
|
@ -784,7 +784,15 @@ describe('workspace_panel', () => {
|
|||
|
||||
function initComponent(draggingContext = draggedField) {
|
||||
instance = mount(
|
||||
<ChildDragDropProvider dragging={draggingContext} setDragging={() => {}}>
|
||||
<ChildDragDropProvider
|
||||
dragging={draggingContext}
|
||||
setDragging={() => {}}
|
||||
setActiveDropTarget={() => {}}
|
||||
activeDropTarget={undefined}
|
||||
keyboardMode={false}
|
||||
setKeyboardMode={() => {}}
|
||||
setA11yMessage={() => {}}
|
||||
>
|
||||
<WorkspacePanel
|
||||
activeDatasourceId={'mock'}
|
||||
datasourceStates={{
|
||||
|
@ -822,7 +830,7 @@ describe('workspace_panel', () => {
|
|||
});
|
||||
initComponent();
|
||||
|
||||
instance.find(DragDrop).prop('onDrop')!(draggedField);
|
||||
instance.find(DragDrop).prop('onDrop')!(draggedField, { id: 'lnsWorkspace' });
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
type: 'SWITCH_VISUALIZATION',
|
||||
|
|
|
@ -39,7 +39,7 @@ import {
|
|||
isLensFilterEvent,
|
||||
isLensEditEvent,
|
||||
} from '../../../types';
|
||||
import { DragDrop, DragContext, Dragging } from '../../../drag_drop';
|
||||
import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop';
|
||||
import { Suggestion, switchToSuggestion } from '../suggestion_helpers';
|
||||
import { buildExpression } from '../expression_helpers';
|
||||
import { debouncedComponent } from '../../../debounced_component';
|
||||
|
@ -75,7 +75,7 @@ export interface WorkspacePanelProps {
|
|||
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
|
||||
title?: string;
|
||||
visualizeTriggerFieldContext?: VisualizeFieldContext;
|
||||
getSuggestionForField: (field: Dragging) => Suggestion | undefined;
|
||||
getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined;
|
||||
}
|
||||
|
||||
interface WorkspaceState {
|
||||
|
@ -83,8 +83,10 @@ interface WorkspaceState {
|
|||
expandError: boolean;
|
||||
}
|
||||
|
||||
const workspaceDropValue = { id: 'lnsWorkspace' };
|
||||
|
||||
// Exported for testing purposes only.
|
||||
export function WorkspacePanel({
|
||||
export const WorkspacePanel = React.memo(function WorkspacePanel({
|
||||
activeDatasourceId,
|
||||
activeVisualizationId,
|
||||
visualizationMap,
|
||||
|
@ -102,7 +104,8 @@ export function WorkspacePanel({
|
|||
}: WorkspacePanelProps) {
|
||||
const dragDropContext = useContext(DragContext);
|
||||
|
||||
const suggestionForDraggedField = getSuggestionForField(dragDropContext.dragging);
|
||||
const suggestionForDraggedField =
|
||||
dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging);
|
||||
|
||||
const [localState, setLocalState] = useState<WorkspaceState>({
|
||||
expressionBuildError: undefined,
|
||||
|
@ -296,10 +299,11 @@ export function WorkspacePanel({
|
|||
>
|
||||
<DragDrop
|
||||
className="lnsWorkspacePanel__dragDrop"
|
||||
data-test-subj="lnsWorkspace"
|
||||
dataTestSubj="lnsWorkspace"
|
||||
draggable={false}
|
||||
droppable={Boolean(suggestionForDraggedField)}
|
||||
onDrop={onDrop}
|
||||
value={workspaceDropValue}
|
||||
>
|
||||
<div>
|
||||
{renderVisualization()}
|
||||
|
@ -308,7 +312,7 @@ export function WorkspacePanel({
|
|||
</DragDrop>
|
||||
</WorkspacePanelWrapper>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export const InnerVisualizationWrapper = ({
|
||||
expression,
|
||||
|
|
|
@ -278,7 +278,10 @@ describe('IndexPattern Data Panel', () => {
|
|||
{...defaultProps}
|
||||
state={state}
|
||||
setState={setStateSpy}
|
||||
dragDropContext={{ dragging: { id: '1' }, setDragging: () => {} }}
|
||||
dragDropContext={{
|
||||
...createMockedDragDropContext(),
|
||||
dragging: { id: '1' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -297,7 +300,10 @@ describe('IndexPattern Data Panel', () => {
|
|||
indexPatterns: {},
|
||||
}}
|
||||
setState={jest.fn()}
|
||||
dragDropContext={{ dragging: { id: '1' }, setDragging: () => {} }}
|
||||
dragDropContext={{
|
||||
...createMockedDragDropContext(),
|
||||
dragging: { id: '1' },
|
||||
}}
|
||||
changeIndexPattern={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
@ -329,7 +335,10 @@ describe('IndexPattern Data Panel', () => {
|
|||
...defaultProps,
|
||||
changeIndexPattern: jest.fn(),
|
||||
setState,
|
||||
dragDropContext: { dragging: { id: '1' }, setDragging: () => {} },
|
||||
dragDropContext: {
|
||||
...createMockedDragDropContext(),
|
||||
dragging: { id: '1' },
|
||||
},
|
||||
dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' },
|
||||
state: {
|
||||
indexPatternRefs: [],
|
||||
|
|
|
@ -426,6 +426,23 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
);
|
||||
}, [unfilteredFieldGroups, localState.nameFilter, localState.typeFilter]);
|
||||
|
||||
const checkFieldExists = useCallback(
|
||||
(field) =>
|
||||
field.type === 'document' ||
|
||||
fieldExists(existingFields, currentIndexPattern.title, field.name),
|
||||
[existingFields, currentIndexPattern.title]
|
||||
);
|
||||
|
||||
const { nameFilter, typeFilter } = localState;
|
||||
|
||||
const filter = useMemo(
|
||||
() => ({
|
||||
nameFilter,
|
||||
typeFilter,
|
||||
}),
|
||||
[nameFilter, typeFilter]
|
||||
);
|
||||
|
||||
const fieldProps = useMemo(
|
||||
() => ({
|
||||
core,
|
||||
|
@ -586,17 +603,11 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
</EuiScreenReaderOnly>
|
||||
<EuiFlexItem>
|
||||
<FieldList
|
||||
exists={(field) =>
|
||||
field.type === 'document' ||
|
||||
fieldExists(existingFields, currentIndexPattern.title, field.name)
|
||||
}
|
||||
exists={checkFieldExists}
|
||||
fieldProps={fieldProps}
|
||||
fieldGroups={fieldGroups}
|
||||
hasSyncedExistingFields={!!hasSyncedExistingFields}
|
||||
filter={{
|
||||
nameFilter: localState.nameFilter,
|
||||
typeFilter: localState.typeFilter,
|
||||
}}
|
||||
filter={filter}
|
||||
currentIndexPatternId={currentIndexPatternId}
|
||||
existenceFetchFailed={existenceFetchFailed}
|
||||
existFieldsInIndex={!!allFields.length}
|
||||
|
|
|
@ -316,6 +316,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
droppedItem: dragging,
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
groupId: '1',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
|
@ -352,6 +353,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
droppedItem: dragging,
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => op.isBucketed,
|
||||
groupId: '1',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
|
@ -387,6 +389,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
},
|
||||
droppedItem: dragging,
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
groupId: '1',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
|
@ -438,6 +441,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
groupId: '1',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
|
@ -473,6 +477,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
},
|
||||
droppedItem: dragging,
|
||||
columnId: 'col2',
|
||||
groupId: '1',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
|
@ -538,6 +543,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
},
|
||||
droppedItem: dragging,
|
||||
state: testState,
|
||||
groupId: '1',
|
||||
});
|
||||
|
||||
expect(setState).toBeCalledTimes(1);
|
||||
|
@ -600,6 +606,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
droppedItem: dragging,
|
||||
state: testState,
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
groupId: 'a',
|
||||
};
|
||||
|
||||
const stateWithColumnOrder = (columnOrder: string[]) => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
DatasourceDimensionDropProps,
|
||||
DatasourceDimensionDropHandlerProps,
|
||||
isDraggedOperation,
|
||||
DraggedOperation,
|
||||
} from '../../types';
|
||||
import { IndexPatternColumn } from '../indexpattern';
|
||||
import { insertOrReplaceColumn } from '../operations';
|
||||
|
@ -15,7 +16,15 @@ import { mergeLayer } from '../state_helpers';
|
|||
import { hasField, isDraggedField } from '../utils';
|
||||
import { IndexPatternPrivateState, IndexPatternField } from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { getOperationSupportMatrix } from './operation_support';
|
||||
import { getOperationSupportMatrix, OperationSupportMatrix } from './operation_support';
|
||||
|
||||
type DropHandlerProps<T = DraggedOperation> = Pick<
|
||||
DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>,
|
||||
'columnId' | 'setState' | 'state' | 'layerId' | 'droppedItem'
|
||||
> & {
|
||||
droppedItem: T;
|
||||
operationSupportMatrix: OperationSupportMatrix;
|
||||
};
|
||||
|
||||
export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
|
@ -29,11 +38,11 @@ export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPr
|
|||
|
||||
if (isDraggedField(dragging)) {
|
||||
const currentColumn = props.state.layers[props.layerId].columns[props.columnId];
|
||||
return (
|
||||
return Boolean(
|
||||
layerIndexPatternId === dragging.indexPatternId &&
|
||||
Boolean(hasOperationForField(dragging.field)) &&
|
||||
(!currentColumn ||
|
||||
(hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name))
|
||||
Boolean(hasOperationForField(dragging.field)) &&
|
||||
(!currentColumn ||
|
||||
(hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -59,74 +68,80 @@ function reorderElements(items: string[], dest: string, src: string) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
const { setState, state, layerId, columnId, droppedItem } = props;
|
||||
const onReorderDrop = ({ columnId, setState, state, layerId, droppedItem }: DropHandlerProps) => {
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
columnOrder: reorderElements(
|
||||
state.layers[layerId].columnOrder,
|
||||
columnId,
|
||||
droppedItem.columnId
|
||||
),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (isDraggedOperation(droppedItem) && props.isReorder) {
|
||||
const dropEl = columnId;
|
||||
return true;
|
||||
};
|
||||
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
columnOrder: reorderElements(
|
||||
state.layers[layerId].columnOrder,
|
||||
dropEl,
|
||||
droppedItem.columnId
|
||||
),
|
||||
},
|
||||
})
|
||||
);
|
||||
const onMoveDropToCompatibleGroup = ({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
}: DropHandlerProps) => {
|
||||
const layer = state.layers[layerId];
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
const newColumns = { ...layer.columns };
|
||||
delete newColumns[droppedItem.columnId];
|
||||
newColumns[columnId] = op;
|
||||
|
||||
return true;
|
||||
const newColumnOrder = [...layer.columnOrder];
|
||||
const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId);
|
||||
const newIndex = newColumnOrder.findIndex((c) => c === columnId);
|
||||
|
||||
if (newIndex === -1) {
|
||||
newColumnOrder[oldIndex] = columnId;
|
||||
} else {
|
||||
newColumnOrder.splice(oldIndex, 1);
|
||||
}
|
||||
|
||||
// Time to replace
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
columnOrder: newColumnOrder,
|
||||
columns: newColumns,
|
||||
},
|
||||
})
|
||||
);
|
||||
return { deleted: droppedItem.columnId };
|
||||
};
|
||||
|
||||
const onFieldDrop = ({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
operationSupportMatrix,
|
||||
}: DropHandlerProps<unknown>) => {
|
||||
function hasOperationForField(field: IndexPatternField) {
|
||||
return Boolean(operationSupportMatrix.operationByField[field.name]);
|
||||
}
|
||||
|
||||
if (isDraggedOperation(droppedItem) && droppedItem.layerId === layerId) {
|
||||
const layer = state.layers[layerId];
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
if (!props.filterOperations(op)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newColumns = { ...layer.columns };
|
||||
delete newColumns[droppedItem.columnId];
|
||||
newColumns[columnId] = op;
|
||||
|
||||
const newColumnOrder = [...layer.columnOrder];
|
||||
const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId);
|
||||
const newIndex = newColumnOrder.findIndex((c) => c === columnId);
|
||||
|
||||
if (newIndex === -1) {
|
||||
newColumnOrder[oldIndex] = columnId;
|
||||
} else {
|
||||
newColumnOrder.splice(oldIndex, 1);
|
||||
}
|
||||
|
||||
// Time to replace
|
||||
setState(
|
||||
mergeLayer({
|
||||
state,
|
||||
layerId,
|
||||
newLayer: {
|
||||
columnOrder: newColumnOrder,
|
||||
columns: newColumns,
|
||||
},
|
||||
})
|
||||
);
|
||||
return { deleted: droppedItem.columnId };
|
||||
}
|
||||
|
||||
if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) {
|
||||
// TODO: What do we do if we couldn't find a column?
|
||||
return false;
|
||||
}
|
||||
|
||||
// dragged field, not operation
|
||||
|
||||
const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name];
|
||||
|
||||
if (!operationsForNewField || operationsForNewField.size === 0) {
|
||||
|
@ -159,6 +174,56 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPr
|
|||
const hasData = Object.values(state.layers).some(({ columns }) => columns.length);
|
||||
trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty');
|
||||
setState(mergeLayer({ state, layerId, newLayer }));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
|
||||
const operationSupportMatrix = getOperationSupportMatrix(props);
|
||||
const { setState, state, droppedItem, columnId, layerId, groupId, isNew } = props;
|
||||
|
||||
if (!isDraggedOperation(droppedItem)) {
|
||||
return onFieldDrop({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
operationSupportMatrix,
|
||||
});
|
||||
}
|
||||
const isExistingFromSameGroup =
|
||||
droppedItem.groupId === groupId && droppedItem.columnId !== columnId && !isNew;
|
||||
|
||||
// reorder in the same group
|
||||
if (isExistingFromSameGroup) {
|
||||
return onReorderDrop({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
operationSupportMatrix,
|
||||
});
|
||||
}
|
||||
|
||||
// replace or move to compatible group
|
||||
const isFromOtherGroup = droppedItem.groupId !== groupId && droppedItem.layerId === layerId;
|
||||
|
||||
if (isFromOtherGroup) {
|
||||
const layer = state.layers[layerId];
|
||||
const op = { ...layer.columns[droppedItem.columnId] };
|
||||
|
||||
if (props.filterOperations(op)) {
|
||||
return onMoveDropToCompatibleGroup({
|
||||
columnId,
|
||||
setState,
|
||||
state,
|
||||
layerId,
|
||||
droppedItem,
|
||||
operationSupportMatrix,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -95,6 +95,8 @@ describe('IndexPattern Field Item', () => {
|
|||
},
|
||||
exists: true,
|
||||
chartsThemeService,
|
||||
groupIndex: 0,
|
||||
itemIndex: 0,
|
||||
dropOntoWorkspace: () => {},
|
||||
hasSuggestionForField: () => false,
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import './field_item.scss';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import DateMath from '@elastic/datemath';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
|
@ -48,7 +48,7 @@ import {
|
|||
import { FieldButton } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
||||
import { DraggedField } from './indexpattern';
|
||||
import { DragDrop, Dragging } from '../drag_drop';
|
||||
import { DragDrop, DragDropIdentifier } from '../drag_drop';
|
||||
import { DatasourceDataPanelProps, DataType } from '../types';
|
||||
import { BucketedAggregation, FieldStatsResponse } from '../../common';
|
||||
import { IndexPattern, IndexPatternField } from './types';
|
||||
|
@ -69,6 +69,8 @@ export interface FieldItemProps {
|
|||
chartsThemeService: ChartsPluginSetup['theme'];
|
||||
filters: Filter[];
|
||||
hideDetails?: boolean;
|
||||
itemIndex: number;
|
||||
groupIndex: number;
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
|
||||
}
|
||||
|
@ -106,7 +108,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
||||
const dropOntoWorkspaceAndClose = useCallback(
|
||||
(droppedField: Dragging) => {
|
||||
(droppedField: DragDropIdentifier) => {
|
||||
dropOntoWorkspace(droppedField);
|
||||
setOpen(false);
|
||||
},
|
||||
|
@ -163,10 +165,11 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
}
|
||||
}
|
||||
|
||||
const value = React.useMemo(
|
||||
const value = useMemo(
|
||||
() => ({ field, indexPatternId: indexPattern.id, id: field.name } as DraggedField),
|
||||
[field, indexPattern.id]
|
||||
);
|
||||
|
||||
const lensFieldIcon = <LensFieldIcon type={field.type as DataType} />;
|
||||
const lensInfoIcon = (
|
||||
<EuiIconTip
|
||||
|
@ -200,10 +203,11 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
container={document.querySelector<HTMLElement>('.application') || undefined}
|
||||
button={
|
||||
<DragDrop
|
||||
noKeyboardSupportYet
|
||||
draggable
|
||||
label={field.displayName}
|
||||
value={value}
|
||||
data-test-subj={`lnsFieldListPanelField-${field.name}`}
|
||||
draggable
|
||||
dataTestSubj={`lnsFieldListPanelField-${field.name}`}
|
||||
>
|
||||
<FieldButton
|
||||
className={`lnsFieldItem lnsFieldItem--${field.type} lnsFieldItem--${
|
||||
|
@ -215,7 +219,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
['aria-label']: i18n.translate(
|
||||
'xpack.lens.indexPattern.fieldStatsButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: '{fieldName}: {fieldType}. Hit enter for a field preview.',
|
||||
defaultMessage: 'Preview {fieldName}: {fieldType}',
|
||||
values: {
|
||||
fieldName: field.displayName,
|
||||
fieldType: field.type,
|
||||
|
|
|
@ -40,7 +40,7 @@ function getDisplayedFieldsLength(
|
|||
.reduce((allFieldCount, [, { fields }]) => allFieldCount + fields.length, 0);
|
||||
}
|
||||
|
||||
export function FieldList({
|
||||
export const FieldList = React.memo(function FieldList({
|
||||
exists,
|
||||
fieldGroups,
|
||||
existenceFetchFailed,
|
||||
|
@ -135,13 +135,15 @@ export function FieldList({
|
|||
{Object.entries(fieldGroups)
|
||||
.filter(([, { showInAccordion }]) => !showInAccordion)
|
||||
.flatMap(([, { fields }]) =>
|
||||
fields.map((field) => (
|
||||
fields.map((field, index) => (
|
||||
<FieldItem
|
||||
{...fieldProps}
|
||||
exists={exists(field)}
|
||||
field={field}
|
||||
hideDetails={true}
|
||||
key={field.name}
|
||||
itemIndex={index}
|
||||
groupIndex={0}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
/>
|
||||
|
@ -151,7 +153,7 @@ export function FieldList({
|
|||
<EuiSpacer size="s" />
|
||||
{Object.entries(fieldGroups)
|
||||
.filter(([, { showInAccordion }]) => showInAccordion)
|
||||
.map(([key, fieldGroup]) => (
|
||||
.map(([key, fieldGroup], index) => (
|
||||
<Fragment key={key}>
|
||||
<FieldsAccordion
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
|
@ -168,6 +170,7 @@ export function FieldList({
|
|||
isFiltered={fieldGroup.fieldCount !== fieldGroup.fields.length}
|
||||
paginatedFields={paginatedFields[key]}
|
||||
fieldProps={fieldProps}
|
||||
groupIndex={index + 1}
|
||||
onToggle={(open) => {
|
||||
setAccordionState((s) => ({
|
||||
...s,
|
||||
|
@ -198,4 +201,4 @@ export function FieldList({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -72,6 +72,7 @@ describe('Fields Accordion', () => {
|
|||
fieldProps,
|
||||
renderCallout: <div id="lens-test-callout">Callout</div>,
|
||||
exists: () => true,
|
||||
groupIndex: 0,
|
||||
dropOntoWorkspace: () => {},
|
||||
hasSuggestionForField: () => false,
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import './datapanel.scss';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiText,
|
||||
|
@ -50,11 +50,12 @@ export interface FieldsAccordionProps {
|
|||
exists: (field: IndexPatternField) => boolean;
|
||||
showExistenceFetchError?: boolean;
|
||||
hideDetails?: boolean;
|
||||
groupIndex: number;
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
|
||||
}
|
||||
|
||||
export const InnerFieldsAccordion = function InnerFieldsAccordion({
|
||||
export const FieldsAccordion = memo(function InnerFieldsAccordion({
|
||||
initialIsOpen,
|
||||
onToggle,
|
||||
id,
|
||||
|
@ -69,28 +70,72 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
|
|||
exists,
|
||||
hideDetails,
|
||||
showExistenceFetchError,
|
||||
groupIndex,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
}: FieldsAccordionProps) {
|
||||
const renderField = useCallback(
|
||||
(field: IndexPatternField) => (
|
||||
(field: IndexPatternField, index) => (
|
||||
<FieldItem
|
||||
{...fieldProps}
|
||||
key={field.name}
|
||||
field={field}
|
||||
exists={exists(field)}
|
||||
hideDetails={hideDetails}
|
||||
itemIndex={index}
|
||||
groupIndex={groupIndex}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
/>
|
||||
),
|
||||
[fieldProps, exists, hideDetails, dropOntoWorkspace, hasSuggestionForField]
|
||||
[fieldProps, exists, hideDetails, dropOntoWorkspace, hasSuggestionForField, groupIndex]
|
||||
);
|
||||
|
||||
const titleClassname = classNames({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
lnsInnerIndexPatternDataPanel__titleTooltip: !!helpTooltip,
|
||||
});
|
||||
const renderButton = useMemo(() => {
|
||||
const titleClassname = classNames({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
lnsInnerIndexPatternDataPanel__titleTooltip: !!helpTooltip,
|
||||
});
|
||||
return (
|
||||
<EuiText size="xs">
|
||||
<strong className={titleClassname}>{label}</strong>
|
||||
{!!helpTooltip && (
|
||||
<EuiIconTip
|
||||
aria-label={helpTooltip}
|
||||
type="questionInCircle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
position="right"
|
||||
content={helpTooltip}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
);
|
||||
}, [label, helpTooltip]);
|
||||
|
||||
const extraAction = useMemo(() => {
|
||||
return showExistenceFetchError ? (
|
||||
<EuiIconTip
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.existenceErrorAriaLabel', {
|
||||
defaultMessage: 'Existence fetch failed',
|
||||
})}
|
||||
type="alert"
|
||||
color="warning"
|
||||
content={i18n.translate('xpack.lens.indexPattern.existenceErrorLabel', {
|
||||
defaultMessage: "Field information can't be loaded",
|
||||
})}
|
||||
/>
|
||||
) : hasLoaded ? (
|
||||
<EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}>
|
||||
{fieldsCount}
|
||||
</EuiNotificationBadge>
|
||||
) : (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
);
|
||||
}, [showExistenceFetchError, hasLoaded, isFiltered, fieldsCount]);
|
||||
|
||||
return (
|
||||
<EuiAccordion
|
||||
|
@ -98,44 +143,8 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
|
|||
onToggle={onToggle}
|
||||
data-test-subj={id}
|
||||
id={id}
|
||||
buttonContent={
|
||||
<EuiText size="xs">
|
||||
<strong className={titleClassname}>{label}</strong>
|
||||
{!!helpTooltip && (
|
||||
<EuiIconTip
|
||||
aria-label={helpTooltip}
|
||||
type="questionInCircle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
position="right"
|
||||
content={helpTooltip}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
}
|
||||
extraAction={
|
||||
showExistenceFetchError ? (
|
||||
<EuiIconTip
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.existenceErrorAriaLabel', {
|
||||
defaultMessage: 'Existence fetch failed',
|
||||
})}
|
||||
type="alert"
|
||||
color="warning"
|
||||
content={i18n.translate('xpack.lens.indexPattern.existenceErrorLabel', {
|
||||
defaultMessage: "Field information can't be loaded",
|
||||
})}
|
||||
/>
|
||||
) : hasLoaded ? (
|
||||
<EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}>
|
||||
{fieldsCount}
|
||||
</EuiNotificationBadge>
|
||||
) : (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
)
|
||||
}
|
||||
buttonContent={renderButton}
|
||||
extraAction={extraAction}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
{hasLoaded &&
|
||||
|
@ -148,6 +157,4 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({
|
|||
))}
|
||||
</EuiAccordion>
|
||||
);
|
||||
};
|
||||
|
||||
export const FieldsAccordion = memo(InnerFieldsAccordion);
|
||||
});
|
||||
|
|
|
@ -51,11 +51,11 @@ import { mergeLayer } from './state_helpers';
|
|||
import { Datasource, StateSetter } from '../types';
|
||||
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
||||
import { deleteColumn, isReferenced } from './operations';
|
||||
import { Dragging } from '../drag_drop/providers';
|
||||
import { DragDropIdentifier } from '../drag_drop/providers';
|
||||
|
||||
export { OperationType, IndexPatternColumn, deleteColumn } from './operations';
|
||||
|
||||
export type DraggedField = Dragging & {
|
||||
export type DraggedField = DragDropIdentifier & {
|
||||
field: IndexPatternField;
|
||||
indexPatternId: string;
|
||||
};
|
||||
|
|
|
@ -247,5 +247,10 @@ export function createMockedDragDropContext(): jest.Mocked<DragContextState> {
|
|||
return {
|
||||
dragging: undefined,
|
||||
setDragging: jest.fn(),
|
||||
activeDropTarget: undefined,
|
||||
setActiveDropTarget: jest.fn(),
|
||||
keyboardMode: false,
|
||||
setKeyboardMode: jest.fn(),
|
||||
setA11yMessage: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
Datatable,
|
||||
SerializedFieldFormat,
|
||||
} from '../../../../src/plugins/expressions/public';
|
||||
import { DragContextState, Dragging } from './drag_drop';
|
||||
import { DragContextState, DragDropIdentifier } from './drag_drop';
|
||||
import { Document } from './persistence';
|
||||
import { DateRange } from '../common';
|
||||
import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public';
|
||||
|
@ -226,8 +226,8 @@ export interface DatasourceDataPanelProps<T = unknown> {
|
|||
query: Query;
|
||||
dateRange: DateRange;
|
||||
filters: Filter[];
|
||||
dropOntoWorkspace: (field: Dragging) => void;
|
||||
hasSuggestionForField: (field: Dragging) => boolean;
|
||||
dropOntoWorkspace: (field: DragDropIdentifier) => void;
|
||||
hasSuggestionForField: (field: DragDropIdentifier) => boolean;
|
||||
}
|
||||
|
||||
interface SharedDimensionProps {
|
||||
|
@ -301,6 +301,8 @@ export type DatasourceDimensionDropProps<T> = SharedDimensionProps & {
|
|||
|
||||
export type DatasourceDimensionDropHandlerProps<T> = DatasourceDimensionDropProps<T> & {
|
||||
droppedItem: unknown;
|
||||
groupId: string;
|
||||
isNew?: boolean;
|
||||
};
|
||||
|
||||
export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import './xy_config_panel.scss';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState, memo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { debounce } from 'lodash';
|
||||
|
@ -179,8 +179,7 @@ function getValueLabelDisableReason({
|
|||
defaultMessage: 'This setting cannot be changed on stacked or percentage bar charts',
|
||||
});
|
||||
}
|
||||
|
||||
export function XyToolbar(props: VisualizationToolbarProps<State>) {
|
||||
export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProps<State>) {
|
||||
const { state, setState, frame } = props;
|
||||
|
||||
const hasNonBarSeries = state?.layers.some(({ seriesType }) =>
|
||||
|
@ -485,7 +484,8 @@ export function XyToolbar(props: VisualizationToolbarProps<State>) {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const idPrefix = htmlIdGenerator()();
|
||||
|
||||
export function DimensionEditor(
|
||||
|
@ -653,7 +653,7 @@ const ColorPicker = ({
|
|||
}
|
||||
};
|
||||
|
||||
const updateColorInState: EuiColorPickerProps['onChange'] = React.useMemo(
|
||||
const updateColorInState: EuiColorPickerProps['onChange'] = useMemo(
|
||||
() =>
|
||||
debounce((text, output) => {
|
||||
const newYConfigs = [...(layer.yConfig || [])];
|
||||
|
|
|
@ -11188,7 +11188,6 @@
|
|||
"xpack.lens.discover.visualizeFieldLegend": "Visualize フィールド",
|
||||
"xpack.lens.dragDrop.elementLifted": "位置 {position} のアイテム {itemLabel} を持ち上げました。",
|
||||
"xpack.lens.dragDrop.elementMoved": "位置 {prevPosition} から位置 {position} までアイテム {itemLabel} を移動しました",
|
||||
"xpack.lens.dragDrop.reorderInstructions": "スペースバーを押すと、ドラッグを開始します。ドラッグするときには、矢印キーで並べ替えることができます。もう一度スペースバーを押すと終了します。",
|
||||
"xpack.lens.editLayerSettings": "レイヤー設定を編集",
|
||||
"xpack.lens.editLayerSettingsChartType": "レイヤー設定を編集、{chartType}",
|
||||
"xpack.lens.editorFrame.buildExpressionError": "グラフの準備中に予期しないエラーが発生しました",
|
||||
|
|
|
@ -11217,7 +11217,6 @@
|
|||
"xpack.lens.discover.visualizeFieldLegend": "可视化字段",
|
||||
"xpack.lens.dragDrop.elementLifted": "您已将项目 {itemLabel} 提升到位置 {position}",
|
||||
"xpack.lens.dragDrop.elementMoved": "您已将项目 {itemLabel} 从位置 {prevPosition} 移到位置 {position}",
|
||||
"xpack.lens.dragDrop.reorderInstructions": "按空格键开始拖动。拖动时,使用方向键重新排序。再次按空格键结束操作。",
|
||||
"xpack.lens.editLayerSettings": "编辑图层设置",
|
||||
"xpack.lens.editLayerSettingsChartType": "编辑图层设置 {chartType}",
|
||||
"xpack.lens.editorFrame.buildExpressionError": "准备图表时发生意外错误",
|
||||
|
|
|
@ -202,7 +202,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
}) .lnsDragDrop`;
|
||||
const dropping = `[data-test-subj='${dimension}']:nth-of-type(${
|
||||
endIndex + 1
|
||||
}) [data-test-subj='lnsDragDrop-reorderableDrop'`;
|
||||
}) [data-test-subj='lnsDragDrop-reorderableDropLayer'`;
|
||||
await browser.html5DragAndDrop(dragging, dropping);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue