mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
parent
d9828c22d5
commit
90fc153d85
22 changed files with 698 additions and 746 deletions
|
@ -45,6 +45,9 @@
|
|||
.lnsDragDrop-isDropTarget {
|
||||
@include lnsDroppable;
|
||||
@include lnsDroppableActive;
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lnsDragDrop-isActiveGroup {
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
ReorderProvider,
|
||||
DragDropIdentifier,
|
||||
DraggingIdentifier,
|
||||
DropTargets,
|
||||
DropIdentifier,
|
||||
} from './providers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { DropType } from '../types';
|
||||
|
@ -32,6 +32,7 @@ describe('DragDrop', () => {
|
|||
setDragging: jest.fn(),
|
||||
setActiveDropTarget: jest.fn(),
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: undefined,
|
||||
keyboardMode: false,
|
||||
setKeyboardMode: () => {},
|
||||
setA11yMessage: jest.fn(),
|
||||
|
@ -255,11 +256,10 @@ describe('DragDrop', () => {
|
|||
dragging = { id: '1', humanData: { label: 'Label1' } };
|
||||
}}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
activeDropTarget={
|
||||
({ activeDropTarget: value } as unknown) as DragContextState['activeDropTarget']
|
||||
}
|
||||
activeDropTarget={value as DragContextState['activeDropTarget']}
|
||||
keyboardMode={false}
|
||||
setKeyboardMode={(keyboardMode) => true}
|
||||
dropTargetsByOrder={undefined}
|
||||
registerDropTarget={jest.fn()}
|
||||
>
|
||||
<DragDrop
|
||||
|
@ -349,12 +349,10 @@ describe('DragDrop', () => {
|
|||
dragging: { ...items[0].value, ghost: { children: <div />, style: {} } },
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
activeDropTarget: {
|
||||
activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
dropTargetsByOrder: {
|
||||
'2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
'2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' },
|
||||
},
|
||||
activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
dropTargetsByOrder: {
|
||||
'2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
'2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' },
|
||||
},
|
||||
keyboardMode: true,
|
||||
}}
|
||||
|
@ -463,11 +461,9 @@ describe('DragDrop', () => {
|
|||
dragging: { ...items[0].value, ghost: { children: <div>Hello</div>, style: {} } },
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
activeDropTarget: {
|
||||
activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
dropTargetsByOrder: {
|
||||
'2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
},
|
||||
activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
dropTargetsByOrder: {
|
||||
'2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' },
|
||||
},
|
||||
keyboardMode: true,
|
||||
}}
|
||||
|
@ -525,11 +521,12 @@ describe('DragDrop', () => {
|
|||
keyboardMode = mode;
|
||||
}),
|
||||
setActiveDropTarget: (target?: DragDropIdentifier) => {
|
||||
activeDropTarget = { activeDropTarget: target } as DropTargets;
|
||||
activeDropTarget = target as DropIdentifier;
|
||||
},
|
||||
activeDropTarget,
|
||||
setA11yMessage,
|
||||
registerDropTarget,
|
||||
dropTargetsByOrder: undefined,
|
||||
};
|
||||
|
||||
const dragDropSharedProps = {
|
||||
|
@ -665,13 +662,11 @@ describe('DragDrop', () => {
|
|||
const component = mountComponent({
|
||||
dragging: { ...items[0] },
|
||||
keyboardMode: true,
|
||||
activeDropTarget: {
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': undefined,
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': undefined,
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
|
@ -693,15 +688,12 @@ describe('DragDrop', () => {
|
|||
test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => {
|
||||
const component = mountComponent({
|
||||
dragging: { ...items[0] },
|
||||
activeDropTarget: {
|
||||
activeDropTarget: { ...items[2], dropType: 'reorder', onDrop },
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': { ...items[0], onDrop, dropType: 'reorder' },
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
activeDropTarget: { ...items[2], dropType: 'reorder', onDrop },
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': { ...items[0], onDrop, dropType: 'reorder' },
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
|
||||
keyboardMode: true,
|
||||
});
|
||||
const keyboardHandler = component
|
||||
|
@ -747,13 +739,11 @@ describe('DragDrop', () => {
|
|||
const component = mountComponent({
|
||||
dragging: { ...items[0] },
|
||||
keyboardMode: true,
|
||||
activeDropTarget: {
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': undefined,
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: {
|
||||
'2,0,0': undefined,
|
||||
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
setA11yMessage,
|
||||
});
|
||||
|
@ -799,15 +789,13 @@ describe('DragDrop', () => {
|
|||
{...defaultContext}
|
||||
keyboardMode={true}
|
||||
activeDropTarget={{
|
||||
activeDropTarget: {
|
||||
...items[1],
|
||||
onDrop,
|
||||
dropType: 'reorder',
|
||||
},
|
||||
dropTargetsByOrder: {
|
||||
'2,0,1,0': undefined,
|
||||
'2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
},
|
||||
...items[1],
|
||||
onDrop,
|
||||
dropType: 'reorder',
|
||||
}}
|
||||
dropTargetsByOrder={{
|
||||
'2,0,1,0': undefined,
|
||||
'2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' },
|
||||
}}
|
||||
dragging={{ ...items[0] }}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
|
|
|
@ -19,8 +19,8 @@ import {
|
|||
ReorderContext,
|
||||
ReorderState,
|
||||
DropHandler,
|
||||
announce,
|
||||
} from './providers';
|
||||
import { announce } from './announcements';
|
||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||
import { DropType } from '../types';
|
||||
|
||||
|
@ -99,13 +99,15 @@ interface BaseProps {
|
|||
* The props for a draggable instance of that component.
|
||||
*/
|
||||
interface DragInnerProps extends BaseProps {
|
||||
isDragging: boolean;
|
||||
keyboardMode: boolean;
|
||||
setKeyboardMode: DragContextState['setKeyboardMode'];
|
||||
setDragging: DragContextState['setDragging'];
|
||||
setActiveDropTarget: DragContextState['setActiveDropTarget'];
|
||||
setA11yMessage: DragContextState['setA11yMessage'];
|
||||
activeDropTarget: DragContextState['activeDropTarget'];
|
||||
activeDraggingProps?: {
|
||||
keyboardMode: DragContextState['keyboardMode'];
|
||||
activeDropTarget: DragContextState['activeDropTarget'];
|
||||
dropTargetsByOrder: DragContextState['dropTargetsByOrder'];
|
||||
};
|
||||
onDragStart?: (
|
||||
target?:
|
||||
| DroppableEvent['currentTarget']
|
||||
|
@ -121,6 +123,7 @@ interface DragInnerProps extends BaseProps {
|
|||
*/
|
||||
interface DropInnerProps extends BaseProps {
|
||||
dragging: DragContextState['dragging'];
|
||||
keyboardMode: DragContextState['keyboardMode'];
|
||||
setKeyboardMode: DragContextState['setKeyboardMode'];
|
||||
setDragging: DragContextState['setDragging'];
|
||||
setActiveDropTarget: DragContextState['setActiveDropTarget'];
|
||||
|
@ -136,8 +139,9 @@ export const DragDrop = (props: BaseProps) => {
|
|||
const {
|
||||
dragging,
|
||||
setDragging,
|
||||
registerDropTarget,
|
||||
keyboardMode,
|
||||
registerDropTarget,
|
||||
dropTargetsByOrder,
|
||||
setKeyboardMode,
|
||||
activeDropTarget,
|
||||
setActiveDropTarget,
|
||||
|
@ -147,34 +151,31 @@ export const DragDrop = (props: BaseProps) => {
|
|||
const { value, draggable, dropType, reorderableGroup } = props;
|
||||
const isDragging = !!(draggable && value.id === dragging?.id);
|
||||
|
||||
const activeDraggingProps = isDragging
|
||||
? {
|
||||
keyboardMode,
|
||||
activeDropTarget,
|
||||
dropTargetsByOrder,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (draggable && !dropType) {
|
||||
const dragProps = {
|
||||
...props,
|
||||
isDragging,
|
||||
keyboardMode: isDragging ? keyboardMode : false, // optimization to not rerender all dragging components
|
||||
activeDropTarget: isDragging ? activeDropTarget : undefined, // optimization to not rerender all dragging components
|
||||
activeDraggingProps,
|
||||
setKeyboardMode,
|
||||
setDragging,
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
};
|
||||
if (reorderableGroup && reorderableGroup.length > 1) {
|
||||
return (
|
||||
<ReorderableDrag
|
||||
{...dragProps}
|
||||
draggable={draggable}
|
||||
reorderableGroup={reorderableGroup}
|
||||
dragging={dragging}
|
||||
/>
|
||||
);
|
||||
return <ReorderableDrag {...dragProps} reorderableGroup={reorderableGroup} />;
|
||||
} else {
|
||||
return <DragInner {...dragProps} draggable={draggable} />;
|
||||
return <DragInner {...dragProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
const isActiveDropTarget = Boolean(
|
||||
activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id
|
||||
);
|
||||
const isActiveDropTarget = Boolean(activeDropTarget?.id === value.id);
|
||||
const dropProps = {
|
||||
...props,
|
||||
keyboardMode,
|
||||
|
@ -210,9 +211,7 @@ const DragInner = memo(function DragInner({
|
|||
setKeyboardMode,
|
||||
setActiveDropTarget,
|
||||
order,
|
||||
keyboardMode,
|
||||
isDragging,
|
||||
activeDropTarget,
|
||||
activeDraggingProps,
|
||||
dragType,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
|
@ -220,6 +219,10 @@ const DragInner = memo(function DragInner({
|
|||
ariaDescribedBy,
|
||||
setA11yMessage,
|
||||
}: DragInnerProps) {
|
||||
const keyboardMode = activeDraggingProps?.keyboardMode;
|
||||
const activeDropTarget = activeDraggingProps?.activeDropTarget;
|
||||
const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder;
|
||||
|
||||
const dragStart = (
|
||||
e: DroppableEvent | React.KeyboardEvent<HTMLButtonElement>,
|
||||
keyboardModeOn?: boolean
|
||||
|
@ -273,9 +276,9 @@ const DragInner = memo(function DragInner({
|
|||
}
|
||||
};
|
||||
const dropToActiveDropTarget = () => {
|
||||
if (isDragging && activeDropTarget?.activeDropTarget) {
|
||||
if (activeDropTarget) {
|
||||
trackUiEvent('drop_total');
|
||||
const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget.activeDropTarget;
|
||||
const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget;
|
||||
setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType)));
|
||||
onTargetDrop(value, dropType);
|
||||
}
|
||||
|
@ -287,6 +290,7 @@ const DragInner = memo(function DragInner({
|
|||
}
|
||||
|
||||
const nextTarget = nextValidDropTarget(
|
||||
dropTargetsByOrder,
|
||||
activeDropTarget,
|
||||
[order.join(',')],
|
||||
(el) => el?.dropType !== 'reorder',
|
||||
|
@ -301,11 +305,10 @@ const DragInner = memo(function DragInner({
|
|||
);
|
||||
};
|
||||
const shouldShowGhostImageInstead =
|
||||
isDragging &&
|
||||
dragType === 'move' &&
|
||||
keyboardMode &&
|
||||
activeDropTarget?.activeDropTarget &&
|
||||
activeDropTarget?.activeDropTarget.dropType !== 'reorder';
|
||||
activeDropTarget &&
|
||||
activeDropTarget.dropType !== 'reorder';
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, {
|
||||
|
@ -320,7 +323,7 @@ const DragInner = memo(function DragInner({
|
|||
className="lnsDragDrop__keyboardHandler"
|
||||
data-test-subj="lnsDragDrop-keyboardHandler"
|
||||
onBlur={() => {
|
||||
if (isDragging) {
|
||||
if (activeDraggingProps) {
|
||||
dragEnd();
|
||||
}
|
||||
}}
|
||||
|
@ -331,13 +334,13 @@ const DragInner = memo(function DragInner({
|
|||
dropToActiveDropTarget();
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
if (activeDraggingProps) {
|
||||
dragEnd();
|
||||
} else {
|
||||
dragStart(e, true);
|
||||
}
|
||||
} else if (key === keys.ESCAPE) {
|
||||
if (isDragging) {
|
||||
if (activeDraggingProps) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dragEnd();
|
||||
|
@ -357,7 +360,8 @@ const DragInner = memo(function DragInner({
|
|||
'data-test-subj': dataTestSubj || 'lnsDragDrop',
|
||||
className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable', {
|
||||
'lnsDragDrop-isHidden':
|
||||
(isDragging && dragType === 'move' && !keyboardMode) || shouldShowGhostImageInstead,
|
||||
(activeDraggingProps && dragType === 'move' && !keyboardMode) ||
|
||||
shouldShowGhostImageInstead,
|
||||
}),
|
||||
draggable: true,
|
||||
onDragEnd: dragEnd,
|
||||
|
@ -384,19 +388,20 @@ const DropInner = memo(function DropInner(props: DropInnerProps) {
|
|||
isActiveDropTarget,
|
||||
registerDropTarget,
|
||||
setActiveDropTarget,
|
||||
keyboardMode,
|
||||
setKeyboardMode,
|
||||
setDragging,
|
||||
setA11yMessage,
|
||||
} = props;
|
||||
|
||||
useShallowCompareEffect(() => {
|
||||
if (dropType && value && onDrop) {
|
||||
if (dropType && onDrop && keyboardMode) {
|
||||
registerDropTarget(order, { ...value, onDrop, dropType });
|
||||
return () => {
|
||||
registerDropTarget(order, undefined);
|
||||
};
|
||||
}
|
||||
}, [order, value, registerDropTarget, dropType]);
|
||||
}, [order, value, registerDropTarget, dropType, keyboardMode]);
|
||||
|
||||
const classesOnEnter = getAdditionalClassesOnEnter?.(dropType);
|
||||
const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType);
|
||||
|
@ -481,17 +486,19 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
const {
|
||||
value,
|
||||
setActiveDropTarget,
|
||||
keyboardMode,
|
||||
isDragging,
|
||||
activeDropTarget,
|
||||
activeDraggingProps,
|
||||
reorderableGroup,
|
||||
setA11yMessage,
|
||||
} = props;
|
||||
|
||||
const keyboardMode = activeDraggingProps?.keyboardMode;
|
||||
const activeDropTarget = activeDraggingProps?.activeDropTarget;
|
||||
const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder;
|
||||
const isDragging = !!activeDraggingProps;
|
||||
|
||||
const isFocusInGroup = keyboardMode
|
||||
? isDragging &&
|
||||
(!activeDropTarget?.activeDropTarget ||
|
||||
reorderableGroup.some((i) => i.id === activeDropTarget?.activeDropTarget?.id))
|
||||
(!activeDropTarget || reorderableGroup.some((i) => i.id === activeDropTarget?.id))
|
||||
: isDragging;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -530,10 +537,8 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
let activeDropTargetIndex = reorderableGroup.findIndex((i) => i.id === value.id);
|
||||
if (activeDropTarget?.activeDropTarget) {
|
||||
const index = reorderableGroup.findIndex(
|
||||
(i) => i.id === activeDropTarget.activeDropTarget?.id
|
||||
);
|
||||
if (activeDropTarget) {
|
||||
const index = reorderableGroup.findIndex((i) => i.id === activeDropTarget?.id);
|
||||
if (index !== -1) activeDropTargetIndex = index;
|
||||
}
|
||||
if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) {
|
||||
|
@ -542,6 +547,7 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
} else if (keys.ARROW_DOWN === e.key) {
|
||||
if (activeDropTargetIndex < reorderableGroup.length - 1) {
|
||||
const nextTarget = nextValidDropTarget(
|
||||
dropTargetsByOrder,
|
||||
activeDropTarget,
|
||||
[props.order.join(',')],
|
||||
(el) => el?.dropType === 'reorder'
|
||||
|
@ -551,6 +557,7 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
} else if (keys.ARROW_UP === e.key) {
|
||||
if (activeDropTargetIndex > 0) {
|
||||
const nextTarget = nextValidDropTarget(
|
||||
dropTargetsByOrder,
|
||||
activeDropTarget,
|
||||
[props.order.join(',')],
|
||||
(el) => el?.dropType === 'reorder',
|
||||
|
|
|
@ -6,13 +6,8 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DropType } from '../types';
|
||||
export interface HumanData {
|
||||
label: string;
|
||||
groupLabel?: string;
|
||||
position?: number;
|
||||
nextLabel?: string;
|
||||
}
|
||||
import { DropType } from '../../types';
|
||||
import { HumanData } from '.';
|
||||
|
||||
type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string;
|
||||
|
11
x-pack/plugins/lens/public/drag_drop/providers/index.tsx
Normal file
11
x-pack/plugins/lens/public/drag_drop/providers/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './providers';
|
||||
export * from './reorder_provider';
|
||||
export * from './types';
|
||||
export * from './announcements';
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { useContext } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { RootDragDropProvider, DragContext } from './providers';
|
||||
import { RootDragDropProvider, DragContext } from '.';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
|
@ -6,70 +6,15 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HumanData } from './announcements';
|
||||
import { DropType } from '../types';
|
||||
|
||||
/**
|
||||
* A function that handles a drop event.
|
||||
*/
|
||||
export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void;
|
||||
|
||||
export type DragDropIdentifier = Record<string, unknown> & {
|
||||
id: string;
|
||||
/**
|
||||
* The data for accessibility, consists of required label and not required groupLabel and position in group
|
||||
*/
|
||||
humanData: HumanData;
|
||||
};
|
||||
|
||||
export type DraggingIdentifier = DragDropIdentifier & {
|
||||
ghost?: {
|
||||
children: React.ReactElement;
|
||||
style: React.CSSProperties;
|
||||
};
|
||||
};
|
||||
|
||||
export type DropIdentifier = DragDropIdentifier & {
|
||||
dropType: DropType;
|
||||
onDrop: DropHandler;
|
||||
};
|
||||
|
||||
export interface DropTargets {
|
||||
activeDropTarget?: DropIdentifier;
|
||||
dropTargetsByOrder: Record<string, DropIdentifier | undefined>;
|
||||
}
|
||||
/**
|
||||
* The shape of the drag / drop context.
|
||||
*/
|
||||
export interface DragContextState {
|
||||
/**
|
||||
* The item being dragged or undefined.
|
||||
*/
|
||||
dragging?: DraggingIdentifier;
|
||||
|
||||
/**
|
||||
* keyboard mode
|
||||
*/
|
||||
keyboardMode: boolean;
|
||||
/**
|
||||
* keyboard mode
|
||||
*/
|
||||
setKeyboardMode: (mode: boolean) => void;
|
||||
/**
|
||||
* Set the item being dragged.
|
||||
*/
|
||||
setDragging: (dragging?: DraggingIdentifier) => void;
|
||||
|
||||
activeDropTarget?: DropTargets;
|
||||
|
||||
setActiveDropTarget: (newTarget?: DropIdentifier) => void;
|
||||
|
||||
setA11yMessage: (message: string) => void;
|
||||
registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void;
|
||||
}
|
||||
import {
|
||||
DropIdentifier,
|
||||
DraggingIdentifier,
|
||||
DragDropIdentifier,
|
||||
RegisteredDropTargets,
|
||||
DragContextState,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* The drag / drop context singleton, used like so:
|
||||
|
@ -84,51 +29,18 @@ export const DragContext = React.createContext<DragContextState>({
|
|||
activeDropTarget: undefined,
|
||||
setActiveDropTarget: () => {},
|
||||
setA11yMessage: () => {},
|
||||
dropTargetsByOrder: undefined,
|
||||
registerDropTarget: () => {},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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?: DraggingIdentifier;
|
||||
|
||||
/**
|
||||
* Sets the item being dragged. If unspecified, the provider
|
||||
* will behave as if it is the root provider.
|
||||
*/
|
||||
setDragging: (dragging?: DraggingIdentifier) => void;
|
||||
|
||||
activeDropTarget?: {
|
||||
activeDropTarget?: DropIdentifier;
|
||||
dropTargetsByOrder: Record<string, DropIdentifier | undefined>;
|
||||
};
|
||||
|
||||
setActiveDropTarget: (newTarget?: DropIdentifier) => void;
|
||||
|
||||
registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void;
|
||||
|
||||
export interface ProviderProps extends DragContextState {
|
||||
/**
|
||||
* The React children.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
|
||||
setA11yMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -144,13 +56,11 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
});
|
||||
const [keyboardModeState, setKeyboardModeState] = useState(false);
|
||||
const [a11yMessageState, setA11yMessageState] = useState('');
|
||||
const [activeDropTargetState, setActiveDropTargetState] = useState<{
|
||||
activeDropTarget?: DropIdentifier;
|
||||
dropTargetsByOrder: Record<string, DropIdentifier | undefined>;
|
||||
}>({
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: {},
|
||||
});
|
||||
const [activeDropTargetState, setActiveDropTargetState] = useState<DropIdentifier | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const [dropTargetsByOrderState, setDropTargetsByOrderState] = useState<RegisteredDropTargets>({});
|
||||
|
||||
const setDragging = useMemo(
|
||||
() => (dragging?: DraggingIdentifier) => setDraggingState({ dragging }),
|
||||
|
@ -162,24 +72,20 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
]);
|
||||
|
||||
const setActiveDropTarget = useMemo(
|
||||
() => (activeDropTarget?: DropIdentifier) =>
|
||||
setActiveDropTargetState((s) => ({ ...s, activeDropTarget })),
|
||||
() => (activeDropTarget?: DropIdentifier) => setActiveDropTargetState(activeDropTarget),
|
||||
[setActiveDropTargetState]
|
||||
);
|
||||
|
||||
const registerDropTarget = useMemo(
|
||||
() => (order: number[], dropTarget?: DropIdentifier) => {
|
||||
return setActiveDropTargetState((s) => {
|
||||
return setDropTargetsByOrderState((s) => {
|
||||
return {
|
||||
...s,
|
||||
dropTargetsByOrder: {
|
||||
...s.dropTargetsByOrder,
|
||||
[order.join(',')]: dropTarget,
|
||||
},
|
||||
[order.join(',')]: dropTarget,
|
||||
};
|
||||
});
|
||||
},
|
||||
[setActiveDropTargetState]
|
||||
[setDropTargetsByOrderState]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -193,6 +99,7 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
activeDropTarget={activeDropTargetState}
|
||||
setActiveDropTarget={setActiveDropTarget}
|
||||
registerDropTarget={registerDropTarget}
|
||||
dropTargetsByOrder={dropTargetsByOrderState}
|
||||
>
|
||||
{children}
|
||||
</ChildDragDropProvider>
|
||||
|
@ -220,16 +127,17 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
|
|||
}
|
||||
|
||||
export function nextValidDropTarget(
|
||||
activeDropTarget: DropTargets | undefined,
|
||||
dropTargetsByOrder: RegisteredDropTargets,
|
||||
activeDropTarget: DropIdentifier | undefined,
|
||||
draggingOrder: [string],
|
||||
filterElements: (el: DragDropIdentifier) => boolean = () => true,
|
||||
reverse = false
|
||||
) {
|
||||
if (!activeDropTarget) {
|
||||
if (!dropTargetsByOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredTargets = [...Object.entries(activeDropTarget.dropTargetsByOrder)].filter(
|
||||
const filteredTargets = Object.entries(dropTargetsByOrder).filter(
|
||||
([, dropTarget]) => dropTarget && filterElements(dropTarget)
|
||||
);
|
||||
|
||||
|
@ -242,7 +150,7 @@ export function nextValidDropTarget(
|
|||
});
|
||||
|
||||
let currentActiveDropIndex = nextDropTargets.findIndex(
|
||||
([_, dropTarget]) => dropTarget?.id === activeDropTarget?.activeDropTarget?.id
|
||||
([_, dropTarget]) => dropTarget?.id === activeDropTarget?.id
|
||||
);
|
||||
|
||||
if (currentActiveDropIndex === -1) {
|
||||
|
@ -274,6 +182,7 @@ export function ChildDragDropProvider({
|
|||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
registerDropTarget,
|
||||
dropTargetsByOrder,
|
||||
children,
|
||||
}: ProviderProps) {
|
||||
const value = useMemo(
|
||||
|
@ -285,6 +194,7 @@ export function ChildDragDropProvider({
|
|||
activeDropTarget,
|
||||
setActiveDropTarget,
|
||||
setA11yMessage,
|
||||
dropTargetsByOrder,
|
||||
registerDropTarget,
|
||||
}),
|
||||
[
|
||||
|
@ -295,84 +205,9 @@ export function ChildDragDropProvider({
|
|||
setKeyboardMode,
|
||||
keyboardMode,
|
||||
setA11yMessage,
|
||||
dropTargetsByOrder,
|
||||
registerDropTarget,
|
||||
]
|
||||
);
|
||||
return <DragContext.Provider value={value}>{children}</DragContext.Provider>;
|
||||
}
|
||||
|
||||
export interface ReorderState {
|
||||
/**
|
||||
* Ids of the elements that are translated up or down
|
||||
*/
|
||||
reorderedItems: Array<{ id: string; height?: number }>;
|
||||
|
||||
/**
|
||||
* Direction of the move of dragged element in the reordered list
|
||||
*/
|
||||
direction: '-' | '+';
|
||||
/**
|
||||
* height of the dragged element
|
||||
*/
|
||||
draggingHeight: number;
|
||||
/**
|
||||
* indicates that user is in keyboard mode
|
||||
*/
|
||||
isReorderOn: boolean;
|
||||
/**
|
||||
* reorder group needed for screen reader aria-described-by attribute
|
||||
*/
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState;
|
||||
|
||||
export interface ReorderContextState {
|
||||
reorderState: ReorderState;
|
||||
setReorderState: (dispatch: SetReorderStateDispatch) => void;
|
||||
}
|
||||
|
||||
export const ReorderContext = React.createContext<ReorderContextState>({
|
||||
reorderState: {
|
||||
reorderedItems: [],
|
||||
direction: '-',
|
||||
draggingHeight: 40,
|
||||
isReorderOn: false,
|
||||
groupId: '',
|
||||
},
|
||||
setReorderState: () => () => {},
|
||||
});
|
||||
|
||||
export function ReorderProvider({
|
||||
id,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [state, setState] = useState<ReorderContextState['reorderState']>({
|
||||
reorderedItems: [],
|
||||
direction: '-',
|
||||
draggingHeight: 40,
|
||||
isReorderOn: false,
|
||||
groupId: id,
|
||||
});
|
||||
|
||||
const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [
|
||||
setState,
|
||||
]);
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export interface ReorderState {
|
||||
/**
|
||||
* Ids of the elements that are translated up or down
|
||||
*/
|
||||
reorderedItems: Array<{ id: string; height?: number }>;
|
||||
|
||||
/**
|
||||
* Direction of the move of dragged element in the reordered list
|
||||
*/
|
||||
direction: '-' | '+';
|
||||
/**
|
||||
* height of the dragged element
|
||||
*/
|
||||
draggingHeight: number;
|
||||
/**
|
||||
* indicates that user is in keyboard mode
|
||||
*/
|
||||
isReorderOn: boolean;
|
||||
/**
|
||||
* reorder group needed for screen reader aria-described-by attribute
|
||||
*/
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState;
|
||||
|
||||
export interface ReorderContextState {
|
||||
reorderState: ReorderState;
|
||||
setReorderState: (dispatch: SetReorderStateDispatch) => void;
|
||||
}
|
||||
|
||||
export const ReorderContext = React.createContext<ReorderContextState>({
|
||||
reorderState: {
|
||||
reorderedItems: [],
|
||||
direction: '-',
|
||||
draggingHeight: 40,
|
||||
isReorderOn: false,
|
||||
groupId: '',
|
||||
},
|
||||
setReorderState: () => () => {},
|
||||
});
|
||||
|
||||
export function ReorderProvider({
|
||||
id,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [state, setState] = useState<ReorderContextState['reorderState']>({
|
||||
reorderedItems: [],
|
||||
direction: '-',
|
||||
draggingHeight: 40,
|
||||
isReorderOn: false,
|
||||
groupId: id,
|
||||
});
|
||||
|
||||
const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [
|
||||
setState,
|
||||
]);
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
75
x-pack/plugins/lens/public/drag_drop/providers/types.tsx
Normal file
75
x-pack/plugins/lens/public/drag_drop/providers/types.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DropType } from '../../types';
|
||||
|
||||
export interface HumanData {
|
||||
label: string;
|
||||
groupLabel?: string;
|
||||
position?: number;
|
||||
nextLabel?: string;
|
||||
}
|
||||
|
||||
export type DragDropIdentifier = Record<string, unknown> & {
|
||||
id: string;
|
||||
/**
|
||||
* The data for accessibility, consists of required label and not required groupLabel and position in group
|
||||
*/
|
||||
humanData: HumanData;
|
||||
};
|
||||
|
||||
export type DraggingIdentifier = DragDropIdentifier & {
|
||||
ghost?: {
|
||||
children: React.ReactElement;
|
||||
style: React.CSSProperties;
|
||||
};
|
||||
};
|
||||
|
||||
export type DropIdentifier = DragDropIdentifier & {
|
||||
dropType: DropType;
|
||||
onDrop: DropHandler;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that handles a drop event.
|
||||
*/
|
||||
export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void;
|
||||
|
||||
export type RegisteredDropTargets = Record<string, DropIdentifier | undefined> | undefined;
|
||||
|
||||
/**
|
||||
* The shape of the drag / drop context.
|
||||
*/
|
||||
|
||||
export interface DragContextState {
|
||||
/**
|
||||
* The item being dragged or undefined.
|
||||
*/
|
||||
dragging?: DraggingIdentifier;
|
||||
|
||||
/**
|
||||
* keyboard mode
|
||||
*/
|
||||
keyboardMode: boolean;
|
||||
/**
|
||||
* keyboard mode
|
||||
*/
|
||||
setKeyboardMode: (mode: boolean) => void;
|
||||
/**
|
||||
* Set the item being dragged.
|
||||
*/
|
||||
setDragging: (dragging?: DraggingIdentifier) => void;
|
||||
|
||||
activeDropTarget?: DropIdentifier;
|
||||
|
||||
dropTargetsByOrder: RegisteredDropTargets;
|
||||
|
||||
setActiveDropTarget: (newTarget?: DropIdentifier) => void;
|
||||
|
||||
setA11yMessage: (message: string) => void;
|
||||
registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void;
|
||||
}
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop';
|
||||
import React, { useMemo, useCallback, useContext } from 'react';
|
||||
import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop';
|
||||
|
||||
import {
|
||||
Datasource,
|
||||
VisualizationDimensionGroupConfig,
|
||||
|
@ -41,12 +42,10 @@ export function DraggableDimensionButton({
|
|||
group,
|
||||
onDrop,
|
||||
children,
|
||||
dragDropContext,
|
||||
layerDatasourceDropProps,
|
||||
layerDatasource,
|
||||
registerNewButtonRef,
|
||||
}: {
|
||||
dragDropContext: DragContextState;
|
||||
layerId: string;
|
||||
groupIndex: number;
|
||||
layerIndex: number;
|
||||
|
@ -64,8 +63,11 @@ export function DraggableDimensionButton({
|
|||
columnId: string;
|
||||
registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void;
|
||||
}) {
|
||||
const { dragging } = useContext(DragContext);
|
||||
|
||||
const dropProps = layerDatasource.getDropProps({
|
||||
...layerDatasourceDropProps,
|
||||
dragging,
|
||||
columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
groupId: group.groupId,
|
||||
|
@ -105,6 +107,11 @@ export function DraggableDimensionButton({
|
|||
columnId,
|
||||
]);
|
||||
|
||||
const handleOnDrop = React.useCallback(
|
||||
(droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType),
|
||||
[value, onDrop]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={registerNewButtonRefMemoized}
|
||||
|
@ -116,13 +123,11 @@ export function DraggableDimensionButton({
|
|||
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
|
||||
order={[2, layerIndex, groupIndex, accessorIndex]}
|
||||
draggable
|
||||
dragType={isDraggedOperation(dragDropContext.dragging) ? 'move' : 'copy'}
|
||||
dragType={isDraggedOperation(dragging) ? 'move' : 'copy'}
|
||||
dropType={dropType}
|
||||
reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined}
|
||||
value={value}
|
||||
onDrop={(drag: DragDropIdentifier, selectedDropType?: DropType) =>
|
||||
onDrop(drag, value, selectedDropType)
|
||||
}
|
||||
onDrop={handleOnDrop}
|
||||
>
|
||||
{children}
|
||||
</DragDrop>
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import React, { useMemo, useState, useEffect, useContext } from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { generateId } from '../../../id_generator';
|
||||
import { DragDrop, DragDropIdentifier } from '../../../drag_drop';
|
||||
import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop';
|
||||
|
||||
import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../types';
|
||||
import { LayerDatasourceDropProps } from './types';
|
||||
|
||||
|
@ -47,6 +48,8 @@ export function EmptyDimensionButton({
|
|||
layerDatasource: Datasource<unknown, unknown>;
|
||||
layerDatasourceDropProps: LayerDatasourceDropProps;
|
||||
}) {
|
||||
const { dragging } = useContext(DragContext);
|
||||
|
||||
const itemIndex = group.accessors.length;
|
||||
|
||||
const [newColumnId, setNewColumnId] = useState<string>(generateId());
|
||||
|
@ -56,6 +59,7 @@ export function EmptyDimensionButton({
|
|||
|
||||
const dropProps = layerDatasource.getDropProps({
|
||||
...layerDatasourceDropProps,
|
||||
dragging,
|
||||
columnId: newColumnId,
|
||||
filterOperations: group.filterOperations,
|
||||
groupId: group.groupId,
|
||||
|
@ -81,14 +85,18 @@ export function EmptyDimensionButton({
|
|||
[dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel]
|
||||
);
|
||||
|
||||
const handleOnDrop = React.useCallback(
|
||||
(droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType),
|
||||
[value, onDrop]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="lnsLayerPanel__dimensionContainer" data-test-subj={group.dataTestSubj}>
|
||||
<DragDrop
|
||||
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
|
||||
value={value}
|
||||
/* 2 to leave room for data panel and workspace, then go by layer index, then by group index */
|
||||
order={[2, layerIndex, groupIndex, itemIndex]}
|
||||
onDrop={(droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType)}
|
||||
onDrop={handleOnDrop}
|
||||
dropType={dropType}
|
||||
>
|
||||
<div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty">
|
||||
|
|
|
@ -28,6 +28,7 @@ const defaultContext = {
|
|||
setDragging: jest.fn(),
|
||||
setActiveDropTarget: () => {},
|
||||
activeDropTarget: undefined,
|
||||
dropTargetsByOrder: undefined,
|
||||
keyboardMode: false,
|
||||
setKeyboardMode: () => {},
|
||||
setA11yMessage: jest.fn(),
|
||||
|
@ -464,9 +465,7 @@ describe('LayerPanel', () => {
|
|||
|
||||
expect(mockDatasource.getDropProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dragDropContext: expect.objectContaining({
|
||||
dragging: draggingField,
|
||||
}),
|
||||
dragging: draggingField,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -474,9 +473,7 @@ describe('LayerPanel', () => {
|
|||
|
||||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dragDropContext: expect.objectContaining({
|
||||
dragging: draggingField,
|
||||
}),
|
||||
droppedItem: draggingField,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -582,9 +579,7 @@ describe('LayerPanel', () => {
|
|||
|
||||
expect(mockDatasource.getDropProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dragDropContext: expect.objectContaining({
|
||||
dragging: draggingOperation,
|
||||
}),
|
||||
dragging: draggingOperation,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -593,9 +588,7 @@ describe('LayerPanel', () => {
|
|||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: 'b',
|
||||
dragDropContext: expect.objectContaining({
|
||||
dragging: draggingOperation,
|
||||
}),
|
||||
droppedItem: draggingOperation,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -604,9 +597,7 @@ describe('LayerPanel', () => {
|
|||
expect(mockDatasource.onDrop).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: 'newid',
|
||||
dragDropContext: expect.objectContaining({
|
||||
dragging: draggingOperation,
|
||||
}),
|
||||
droppedItem: draggingOperation,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -7,17 +7,12 @@
|
|||
|
||||
import './layer_panel.scss';
|
||||
|
||||
import React, { useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types';
|
||||
import {
|
||||
DragContext,
|
||||
DragDropIdentifier,
|
||||
ChildDragDropProvider,
|
||||
ReorderProvider,
|
||||
} from '../../../drag_drop';
|
||||
import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop';
|
||||
import { LayerSettings } from './layer_settings';
|
||||
import { trackUiEvent } from '../../../lens_ui_telemetry';
|
||||
import { LayerPanelProps, ActiveDimensionState } from './types';
|
||||
|
@ -49,7 +44,6 @@ export function LayerPanel(
|
|||
registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void;
|
||||
}
|
||||
) {
|
||||
const dragDropContext = useContext(DragContext);
|
||||
const [activeDimension, setActiveDimension] = useState<ActiveDimensionState>(
|
||||
initialActiveDimensionState
|
||||
);
|
||||
|
@ -78,7 +72,6 @@ export function LayerPanel(
|
|||
|
||||
const layerVisualizationConfigProps = {
|
||||
layerId,
|
||||
dragDropContext,
|
||||
state: props.visualizationState,
|
||||
frame: props.framePublicAPI,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
|
@ -91,13 +84,12 @@ export function LayerPanel(
|
|||
const layerDatasourceDropProps = useMemo(
|
||||
() => ({
|
||||
layerId,
|
||||
dragDropContext,
|
||||
state: layerDatasourceState,
|
||||
setState: (newState: unknown) => {
|
||||
updateDatasource(datasourceId, newState);
|
||||
},
|
||||
}),
|
||||
[layerId, dragDropContext, layerDatasourceState, datasourceId, updateDatasource]
|
||||
[layerId, layerDatasourceState, datasourceId, updateDatasource]
|
||||
);
|
||||
|
||||
const layerDatasource = props.datasourceMap[datasourceId];
|
||||
|
@ -116,7 +108,6 @@ export function LayerPanel(
|
|||
const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state);
|
||||
|
||||
const { setDimension, removeDimension } = activeVisualization;
|
||||
const layerDatasourceOnDrop = layerDatasource.onDrop;
|
||||
|
||||
const allAccessors = groups.flatMap((group) =>
|
||||
group.accessors.map((accessor) => accessor.columnId)
|
||||
|
@ -128,6 +119,8 @@ export function LayerPanel(
|
|||
registerNewRef: registerNewButtonRef,
|
||||
} = useFocusUpdate(allAccessors);
|
||||
|
||||
const layerDatasourceOnDrop = layerDatasource.onDrop;
|
||||
|
||||
const onDrop = useMemo(() => {
|
||||
return (
|
||||
droppedItem: DragDropIdentifier,
|
||||
|
@ -194,275 +187,272 @@ export function LayerPanel(
|
|||
]);
|
||||
|
||||
return (
|
||||
<ChildDragDropProvider {...dragDropContext}>
|
||||
<section tabIndex={-1} ref={registerLayerRef} className="lnsLayerPanel">
|
||||
<EuiPanel data-test-subj={`lns-layerPanel-${layerIndex}`} paddingSize="s">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
|
||||
<EuiFlexItem grow={false} className="lnsLayerPanel__settingsFlexItem">
|
||||
<LayerSettings
|
||||
layerId={layerId}
|
||||
layerConfigProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
setState: props.updateVisualization,
|
||||
<section tabIndex={-1} ref={registerLayerRef} className="lnsLayerPanel">
|
||||
<EuiPanel data-test-subj={`lns-layerPanel-${layerIndex}`} paddingSize="s">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}>
|
||||
<EuiFlexItem grow={false} className="lnsLayerPanel__settingsFlexItem">
|
||||
<LayerSettings
|
||||
layerId={layerId}
|
||||
layerConfigProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
setState: props.updateVisualization,
|
||||
}}
|
||||
activeVisualization={activeVisualization}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{layerDatasource && (
|
||||
<EuiFlexItem className="lnsLayerPanel__sourceFlexItem">
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderLayerPanel}
|
||||
nativeProps={{
|
||||
layerId,
|
||||
state: layerDatasourceState,
|
||||
activeData: props.framePublicAPI.activeData,
|
||||
setState: (updater: unknown) => {
|
||||
const newState =
|
||||
typeof updater === 'function' ? updater(layerDatasourceState) : updater;
|
||||
// Look for removed columns
|
||||
const nextPublicAPI = layerDatasource.getPublicAPI({
|
||||
state: newState,
|
||||
layerId,
|
||||
});
|
||||
const nextTable = new Set(
|
||||
nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
|
||||
);
|
||||
const removed = datasourcePublicAPI
|
||||
.getTableSpec()
|
||||
.map(({ columnId }) => columnId)
|
||||
.filter((columnId) => !nextTable.has(columnId));
|
||||
let nextVisState = props.visualizationState;
|
||||
removed.forEach((columnId) => {
|
||||
nextVisState = activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId,
|
||||
prevState: nextVisState,
|
||||
});
|
||||
});
|
||||
|
||||
props.updateAll(datasourceId, newState, nextVisState);
|
||||
},
|
||||
}}
|
||||
activeVisualization={activeVisualization}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
{layerDatasource && (
|
||||
<EuiFlexItem className="lnsLayerPanel__sourceFlexItem">
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{groups.map((group, groupIndex) => {
|
||||
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
|
||||
return (
|
||||
<EuiFormRow
|
||||
className={
|
||||
group.supportsMoreColumns
|
||||
? 'lnsLayerPanel__row'
|
||||
: 'lnsLayerPanel__row lnsLayerPanel__row--notSupportsMoreColumns'
|
||||
}
|
||||
fullWidth
|
||||
label={<div className="lnsLayerPanel__groupLabel">{group.groupLabel}</div>}
|
||||
labelType="legend"
|
||||
key={group.groupId}
|
||||
isInvalid={isMissing}
|
||||
error={
|
||||
isMissing ? (
|
||||
<div className="lnsLayerPanel__error">
|
||||
{i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
|
||||
defaultMessage: 'Required dimension',
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
[]
|
||||
)
|
||||
}
|
||||
>
|
||||
<>
|
||||
<ReorderProvider id={group.groupId} className={'lnsLayerPanel__group'}>
|
||||
{group.accessors.map((accessorConfig, accessorIndex) => {
|
||||
const { columnId } = accessorConfig;
|
||||
|
||||
return (
|
||||
<DraggableDimensionButton
|
||||
registerNewButtonRef={registerNewButtonRef}
|
||||
accessorIndex={accessorIndex}
|
||||
columnId={columnId}
|
||||
group={group}
|
||||
groupIndex={groupIndex}
|
||||
key={columnId}
|
||||
layerDatasourceDropProps={layerDatasourceDropProps}
|
||||
label={columnLabelMap[columnId]}
|
||||
layerDatasource={layerDatasource}
|
||||
layerIndex={layerIndex}
|
||||
layerId={layerId}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<div className="lnsLayerPanel__dimension">
|
||||
<DimensionButton
|
||||
accessorConfig={accessorConfig}
|
||||
label={columnLabelMap[accessorConfig.columnId]}
|
||||
group={group}
|
||||
onClick={(id: string) => {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
});
|
||||
}}
|
||||
onRemoveClick={(id: string) => {
|
||||
trackUiEvent('indexpattern_dimension_removed');
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
layerDatasource.removeColumn({
|
||||
layerId,
|
||||
columnId: id,
|
||||
prevState: layerDatasourceState,
|
||||
}),
|
||||
activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: id,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
removeButtonRef(id);
|
||||
}}
|
||||
>
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessorConfig.columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
}}
|
||||
/>
|
||||
</DimensionButton>
|
||||
</div>
|
||||
</DraggableDimensionButton>
|
||||
);
|
||||
})}
|
||||
</ReorderProvider>
|
||||
{group.supportsMoreColumns ? (
|
||||
<EmptyDimensionButton
|
||||
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>
|
||||
);
|
||||
})}
|
||||
<DimensionContainer
|
||||
isOpen={!!activeId}
|
||||
groupLabel={activeGroup?.groupLabel || ''}
|
||||
handleClose={() => {
|
||||
if (layerDatasource.updateStateOnCloseDimension) {
|
||||
const newState = layerDatasource.updateStateOnCloseDimension({
|
||||
state: layerDatasourceState,
|
||||
layerId,
|
||||
columnId: activeId!,
|
||||
});
|
||||
if (newState) {
|
||||
props.updateDatasource(datasourceId, newState);
|
||||
}
|
||||
}
|
||||
setActiveDimension(initialActiveDimensionState);
|
||||
}}
|
||||
panel={
|
||||
<>
|
||||
{activeGroup && activeId && (
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderLayerPanel}
|
||||
render={layerDatasource.renderDimensionEditor}
|
||||
nativeProps={{
|
||||
layerId,
|
||||
state: layerDatasourceState,
|
||||
activeData: props.framePublicAPI.activeData,
|
||||
setState: (updater: unknown) => {
|
||||
const newState =
|
||||
typeof updater === 'function' ? updater(layerDatasourceState) : updater;
|
||||
// Look for removed columns
|
||||
const nextPublicAPI = layerDatasource.getPublicAPI({
|
||||
state: newState,
|
||||
layerId,
|
||||
...layerDatasourceConfigProps,
|
||||
core: props.core,
|
||||
columnId: activeId,
|
||||
filterOperations: activeGroup.filterOperations,
|
||||
dimensionGroups: groups,
|
||||
setState: (
|
||||
newState: unknown,
|
||||
{
|
||||
shouldReplaceDimension,
|
||||
shouldRemoveDimension,
|
||||
}: {
|
||||
shouldReplaceDimension?: boolean;
|
||||
shouldRemoveDimension?: boolean;
|
||||
} = {}
|
||||
) => {
|
||||
if (shouldReplaceDimension || shouldRemoveDimension) {
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
newState,
|
||||
shouldRemoveDimension
|
||||
? activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: activeId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
: activeVisualization.setDimension({
|
||||
layerId,
|
||||
groupId: activeGroup.groupId,
|
||||
columnId: activeId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
props.updateDatasource(datasourceId, newState);
|
||||
}
|
||||
setActiveDimension({
|
||||
...activeDimension,
|
||||
isNew: false,
|
||||
});
|
||||
const nextTable = new Set(
|
||||
nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
|
||||
);
|
||||
const removed = datasourcePublicAPI
|
||||
.getTableSpec()
|
||||
.map(({ columnId }) => columnId)
|
||||
.filter((columnId) => !nextTable.has(columnId));
|
||||
let nextVisState = props.visualizationState;
|
||||
removed.forEach((columnId) => {
|
||||
nextVisState = activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId,
|
||||
prevState: nextVisState,
|
||||
});
|
||||
});
|
||||
|
||||
props.updateAll(datasourceId, newState, nextVisState);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{groups.map((group, groupIndex) => {
|
||||
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
|
||||
return (
|
||||
<EuiFormRow
|
||||
className={
|
||||
group.supportsMoreColumns
|
||||
? 'lnsLayerPanel__row'
|
||||
: 'lnsLayerPanel__row lnsLayerPanel__row--notSupportsMoreColumns'
|
||||
}
|
||||
fullWidth
|
||||
label={<div className="lnsLayerPanel__groupLabel">{group.groupLabel}</div>}
|
||||
labelType="legend"
|
||||
key={group.groupId}
|
||||
isInvalid={isMissing}
|
||||
error={
|
||||
isMissing ? (
|
||||
<div className="lnsLayerPanel__error">
|
||||
{i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
|
||||
defaultMessage: 'Required dimension',
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
[]
|
||||
)
|
||||
}
|
||||
>
|
||||
<>
|
||||
<ReorderProvider id={group.groupId} className={'lnsLayerPanel__group'}>
|
||||
{group.accessors.map((accessorConfig, accessorIndex) => {
|
||||
const { columnId } = accessorConfig;
|
||||
|
||||
return (
|
||||
<DraggableDimensionButton
|
||||
registerNewButtonRef={registerNewButtonRef}
|
||||
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">
|
||||
<DimensionButton
|
||||
accessorConfig={accessorConfig}
|
||||
label={columnLabelMap[accessorConfig.columnId]}
|
||||
group={group}
|
||||
onClick={(id: string) => {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
});
|
||||
}}
|
||||
onRemoveClick={(id: string) => {
|
||||
trackUiEvent('indexpattern_dimension_removed');
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
layerDatasource.removeColumn({
|
||||
layerId,
|
||||
columnId: id,
|
||||
prevState: layerDatasourceState,
|
||||
}),
|
||||
activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: id,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
removeButtonRef(id);
|
||||
}}
|
||||
>
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessorConfig.columnId,
|
||||
filterOperations: group.filterOperations,
|
||||
}}
|
||||
/>
|
||||
</DimensionButton>
|
||||
</div>
|
||||
</DraggableDimensionButton>
|
||||
);
|
||||
})}
|
||||
</ReorderProvider>
|
||||
{group.supportsMoreColumns ? (
|
||||
<EmptyDimensionButton
|
||||
group={group}
|
||||
groupIndex={groupIndex}
|
||||
layerId={layerId}
|
||||
layerIndex={layerIndex}
|
||||
layerDatasource={layerDatasource}
|
||||
layerDatasourceDropProps={layerDatasourceDropProps}
|
||||
onClick={(id) => {
|
||||
setActiveDimension({
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
isNew: true,
|
||||
});
|
||||
)}
|
||||
{activeGroup &&
|
||||
activeId &&
|
||||
!activeDimension.isNew &&
|
||||
activeVisualization.renderDimensionEditor &&
|
||||
activeGroup?.enableDimensionEditor && (
|
||||
<div className="lnsLayerPanel__styleEditor">
|
||||
<NativeRenderer
|
||||
render={activeVisualization.renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
groupId: activeGroup.groupId,
|
||||
accessor: activeId,
|
||||
setState: props.updateVisualization,
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
})}
|
||||
<DimensionContainer
|
||||
isOpen={!!activeId}
|
||||
groupLabel={activeGroup?.groupLabel || ''}
|
||||
handleClose={() => {
|
||||
if (layerDatasource.updateStateOnCloseDimension) {
|
||||
const newState = layerDatasource.updateStateOnCloseDimension({
|
||||
state: layerDatasourceState,
|
||||
layerId,
|
||||
columnId: activeId!,
|
||||
});
|
||||
if (newState) {
|
||||
props.updateDatasource(datasourceId, newState);
|
||||
}
|
||||
}
|
||||
setActiveDimension(initialActiveDimensionState);
|
||||
}}
|
||||
panel={
|
||||
<>
|
||||
{activeGroup && activeId && (
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
core: props.core,
|
||||
columnId: activeId,
|
||||
filterOperations: activeGroup.filterOperations,
|
||||
dimensionGroups: groups,
|
||||
setState: (
|
||||
newState: unknown,
|
||||
{
|
||||
shouldReplaceDimension,
|
||||
shouldRemoveDimension,
|
||||
}: {
|
||||
shouldReplaceDimension?: boolean;
|
||||
shouldRemoveDimension?: boolean;
|
||||
} = {}
|
||||
) => {
|
||||
if (shouldReplaceDimension || shouldRemoveDimension) {
|
||||
props.updateAll(
|
||||
datasourceId,
|
||||
newState,
|
||||
shouldRemoveDimension
|
||||
? activeVisualization.removeDimension({
|
||||
layerId,
|
||||
columnId: activeId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
: activeVisualization.setDimension({
|
||||
layerId,
|
||||
groupId: activeGroup.groupId,
|
||||
columnId: activeId,
|
||||
prevState: props.visualizationState,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
props.updateDatasource(datasourceId, newState);
|
||||
}
|
||||
setActiveDimension({
|
||||
...activeDimension,
|
||||
isNew: false,
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeGroup &&
|
||||
activeId &&
|
||||
!activeDimension.isNew &&
|
||||
activeVisualization.renderDimensionEditor &&
|
||||
activeGroup?.enableDimensionEditor && (
|
||||
<div className="lnsLayerPanel__styleEditor">
|
||||
<NativeRenderer
|
||||
render={activeVisualization.renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
groupId: activeGroup.groupId,
|
||||
accessor: activeId,
|
||||
setState: props.updateVisualization,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveLayerButton
|
||||
onRemoveLayer={onRemoveLayer}
|
||||
layerIndex={layerIndex}
|
||||
isOnlyLayer={isOnlyLayer}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</section>
|
||||
</ChildDragDropProvider>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveLayerButton
|
||||
onRemoveLayer={onRemoveLayer}
|
||||
layerIndex={layerIndex}
|
||||
isOnlyLayer={isOnlyLayer}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
DatasourceDimensionEditorProps,
|
||||
VisualizationDimensionGroupConfig,
|
||||
} from '../../../types';
|
||||
import { DragContextState } from '../../../drag_drop';
|
||||
export interface ConfigPanelWrapperProps {
|
||||
activeDatasourceId: string;
|
||||
visualizationState: unknown;
|
||||
|
@ -51,7 +50,6 @@ export interface LayerPanelProps {
|
|||
|
||||
export interface LayerDatasourceDropProps {
|
||||
layerId: string;
|
||||
dragDropContext: DragContextState;
|
||||
state: unknown;
|
||||
setState: (newState: unknown) => void;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import './chart_switch.scss';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, memo } from 'react';
|
||||
import {
|
||||
EuiIcon,
|
||||
EuiPopover,
|
||||
|
@ -79,7 +79,7 @@ function VisualizationSummary(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export function ChartSwitch(props: Props) {
|
||||
export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
||||
const [flyoutOpen, setFlyoutOpen] = useState<boolean>(false);
|
||||
|
||||
const commitSelection = (selection: VisualizationSelection) => {
|
||||
|
@ -305,7 +305,7 @@ export function ChartSwitch(props: Props) {
|
|||
);
|
||||
|
||||
return <div className="lnsChartSwitch__header">{popover}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
function getTopSuggestion(
|
||||
props: Props,
|
||||
|
|
|
@ -794,6 +794,7 @@ describe('workspace_panel', () => {
|
|||
setKeyboardMode={() => {}}
|
||||
setA11yMessage={() => {}}
|
||||
registerDropTarget={jest.fn()}
|
||||
dropTargetsByOrder={undefined}
|
||||
>
|
||||
<WorkspacePanel
|
||||
activeDatasourceId={'mock'}
|
||||
|
|
|
@ -88,7 +88,23 @@ const dropProps = {
|
|||
};
|
||||
|
||||
// Exported for testing purposes only.
|
||||
export const WorkspacePanel = React.memo(function WorkspacePanel({
|
||||
export const WorkspacePanel = React.memo(function WorkspacePanel(props: WorkspacePanelProps) {
|
||||
const { getSuggestionForField, ...restProps } = props;
|
||||
|
||||
const dragDropContext = useContext(DragContext);
|
||||
|
||||
const suggestionForDraggedField = useMemo(
|
||||
() => dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging),
|
||||
[dragDropContext.dragging, getSuggestionForField]
|
||||
);
|
||||
|
||||
return (
|
||||
<InnerWorkspacePanel {...restProps} suggestionForDraggedField={suggestionForDraggedField} />
|
||||
);
|
||||
});
|
||||
|
||||
// Exported for testing purposes only.
|
||||
export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
||||
activeDatasourceId,
|
||||
activeVisualizationId,
|
||||
visualizationMap,
|
||||
|
@ -102,13 +118,10 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({
|
|||
ExpressionRenderer: ExpressionRendererComponent,
|
||||
title,
|
||||
visualizeTriggerFieldContext,
|
||||
getSuggestionForField,
|
||||
}: WorkspacePanelProps) {
|
||||
const dragDropContext = useContext(DragContext);
|
||||
|
||||
const suggestionForDraggedField =
|
||||
dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging);
|
||||
|
||||
suggestionForDraggedField,
|
||||
}: Omit<WorkspacePanelProps, 'getSuggestionForField'> & {
|
||||
suggestionForDraggedField: Suggestion | undefined;
|
||||
}) {
|
||||
const [localState, setLocalState] = useState<WorkspaceState>({
|
||||
expressionBuildError: undefined,
|
||||
expandError: false,
|
||||
|
@ -173,6 +186,8 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({
|
|||
]
|
||||
);
|
||||
|
||||
const expressionExists = Boolean(expression);
|
||||
|
||||
const onEvent = useCallback(
|
||||
(event: ExpressionRendererEvent) => {
|
||||
if (!plugins.uiActions) {
|
||||
|
@ -202,23 +217,23 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({
|
|||
|
||||
useEffect(() => {
|
||||
// reset expression error if component attempts to run it again
|
||||
if (expression && localState.expressionBuildError) {
|
||||
if (expressionExists && localState.expressionBuildError) {
|
||||
setLocalState((s) => ({
|
||||
...s,
|
||||
expressionBuildError: undefined,
|
||||
}));
|
||||
}
|
||||
}, [expression, localState.expressionBuildError]);
|
||||
}, [expressionExists, localState.expressionBuildError]);
|
||||
|
||||
function onDrop() {
|
||||
const onDrop = useCallback(() => {
|
||||
if (suggestionForDraggedField) {
|
||||
trackUiEvent('drop_onto_workspace');
|
||||
trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty');
|
||||
trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty');
|
||||
switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION');
|
||||
}
|
||||
}
|
||||
}, [suggestionForDraggedField, expressionExists, dispatch]);
|
||||
|
||||
function renderEmptyWorkspace() {
|
||||
const renderEmptyWorkspace = () => {
|
||||
return (
|
||||
<EuiText
|
||||
className={classNames('lnsWorkspacePanel__emptyContent')}
|
||||
|
@ -229,7 +244,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({
|
|||
>
|
||||
<h2>
|
||||
<strong>
|
||||
{expression === null
|
||||
{!expressionExists
|
||||
? i18n.translate('xpack.lens.editorFrame.emptyWorkspace', {
|
||||
defaultMessage: 'Drop some fields here to start',
|
||||
})
|
||||
|
@ -239,7 +254,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({
|
|||
</strong>
|
||||
</h2>
|
||||
<DropIllustration aria-hidden={true} className="lnsWorkspacePanel__dropIllustration" />
|
||||
{expression === null && (
|
||||
{!expressionExists && (
|
||||
<>
|
||||
<p>
|
||||
{i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', {
|
||||
|
@ -263,9 +278,9 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({
|
|||
)}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function renderVisualization() {
|
||||
const renderVisualization = () => {
|
||||
// we don't want to render the emptyWorkspace on visualizing field from Discover
|
||||
// as it is specific for the drag and drop functionality and can confuse the users
|
||||
if (expression === null && !visualizeTriggerFieldContext) {
|
||||
|
@ -283,7 +298,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({
|
|||
ExpressionRendererComponent={ExpressionRendererComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkspacePanelWrapper
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
|
||||
import { IndexPatternDimensionEditorProps } from './dimension_panel';
|
||||
import { onDrop, getDropProps } from './droppable';
|
||||
import { DragContextState } from '../../drag_drop';
|
||||
import { createMockedDragDropContext } from '../mocks';
|
||||
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { IndexPatternPrivateState } from '../types';
|
||||
|
@ -98,7 +96,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
let state: IndexPatternPrivateState;
|
||||
let setState: jest.Mock;
|
||||
let defaultProps: IndexPatternDimensionEditorProps;
|
||||
let dragDropContext: DragContextState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
|
@ -140,8 +137,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
setState = jest.fn();
|
||||
|
||||
dragDropContext = createMockedDragDropContext();
|
||||
|
||||
defaultProps = {
|
||||
state,
|
||||
setState,
|
||||
|
@ -174,24 +169,28 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
const groupId = 'a';
|
||||
|
||||
describe('getDropProps', () => {
|
||||
it('returns undefined if no drag is happening', () => {
|
||||
expect(getDropProps({ ...defaultProps, groupId, dragDropContext })).toBe(undefined);
|
||||
const dragging = {
|
||||
name: 'bar',
|
||||
id: 'bar',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
expect(getDropProps({ ...defaultProps, groupId, dragging })).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined if the dragged item has no field', () => {
|
||||
const dragging = {
|
||||
name: 'bar',
|
||||
id: 'bar',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
expect(
|
||||
getDropProps({
|
||||
...defaultProps,
|
||||
groupId,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
name: 'bar',
|
||||
id: 'bar',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
},
|
||||
dragging,
|
||||
})
|
||||
).toBe(undefined);
|
||||
});
|
||||
|
@ -201,14 +200,11 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
getDropProps({
|
||||
...defaultProps,
|
||||
groupId,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
indexPatternId: 'foo',
|
||||
field: { type: 'string', name: 'mystring', aggregatable: true },
|
||||
id: 'mystring',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
dragging: {
|
||||
indexPatternId: 'foo',
|
||||
field: { type: 'string', name: 'mystring', aggregatable: true },
|
||||
id: 'mystring',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
filterOperations: () => false,
|
||||
})
|
||||
|
@ -220,10 +216,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
getDropProps({
|
||||
...defaultProps,
|
||||
groupId,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: draggingField,
|
||||
},
|
||||
dragging: draggingField,
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
})
|
||||
).toEqual({ dropType: 'field_replace', nextLabel: 'Intervals' });
|
||||
|
@ -234,14 +227,11 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
getDropProps({
|
||||
...defaultProps,
|
||||
groupId,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
field: { type: 'number', name: 'bar', aggregatable: true },
|
||||
indexPatternId: 'foo2',
|
||||
id: 'bar',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
dragging: {
|
||||
field: { type: 'number', name: 'bar', aggregatable: true },
|
||||
indexPatternId: 'foo2',
|
||||
id: 'bar',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
})
|
||||
|
@ -253,21 +243,18 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
getDropProps({
|
||||
...defaultProps,
|
||||
groupId,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
field: {
|
||||
name: 'timestamp',
|
||||
displayName: 'timestampLabel',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
exists: true,
|
||||
},
|
||||
indexPatternId: 'foo',
|
||||
id: 'bar',
|
||||
humanData: { label: 'Label' },
|
||||
dragging: {
|
||||
field: {
|
||||
name: 'timestamp',
|
||||
displayName: 'timestampLabel',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
exists: true,
|
||||
},
|
||||
indexPatternId: 'foo',
|
||||
id: 'bar',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
})
|
||||
).toBe(undefined);
|
||||
|
@ -278,15 +265,12 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
getDropProps({
|
||||
...defaultProps,
|
||||
groupId,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'b',
|
||||
layerId: 'first',
|
||||
id: 'col1',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'b',
|
||||
layerId: 'first',
|
||||
id: 'col1',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
columnId: 'col2',
|
||||
})
|
||||
|
@ -321,16 +305,14 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
getDropProps({
|
||||
...defaultProps,
|
||||
groupId,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'b',
|
||||
layerId: 'first',
|
||||
id: 'col1',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'b',
|
||||
layerId: 'first',
|
||||
id: 'col1',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
|
||||
columnId: 'col2',
|
||||
})
|
||||
).toEqual(undefined);
|
||||
|
@ -360,15 +342,12 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
getDropProps({
|
||||
...defaultProps,
|
||||
groupId,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'b',
|
||||
layerId: 'first',
|
||||
id: 'col1',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
dragging: {
|
||||
columnId: 'col1',
|
||||
groupId: 'b',
|
||||
layerId: 'first',
|
||||
id: 'col1',
|
||||
humanData: { label: 'Label' },
|
||||
},
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => op.isBucketed === false,
|
||||
|
@ -380,10 +359,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
it('appends the dropped column when a field is dropped', () => {
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: draggingField,
|
||||
},
|
||||
droppedItem: draggingField,
|
||||
dropType: 'field_replace',
|
||||
columnId: 'col2',
|
||||
|
@ -412,10 +387,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
it('selects the specific operation that was valid on drop', () => {
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: draggingField,
|
||||
},
|
||||
droppedItem: draggingField,
|
||||
columnId: 'col2',
|
||||
filterOperations: (op: OperationMetadata) => op.isBucketed,
|
||||
|
@ -444,10 +415,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
it('updates a column when a field is dropped', () => {
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: draggingField,
|
||||
},
|
||||
droppedItem: draggingField,
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
dropType: 'field_replace',
|
||||
|
@ -470,18 +437,8 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
});
|
||||
|
||||
it('keeps the operation when dropping a different compatible field', () => {
|
||||
const dragging = {
|
||||
field: { name: 'memory', type: 'number', aggregatable: true },
|
||||
indexPatternId: 'foo',
|
||||
id: '1',
|
||||
humanData: { label: 'Label' },
|
||||
};
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
droppedItem: {
|
||||
field: { name: 'memory', type: 'number', aggregatable: true },
|
||||
indexPatternId: 'foo',
|
||||
|
@ -538,10 +495,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
droppedItem: dragging,
|
||||
columnId: 'col2',
|
||||
dropType: 'move_compatible',
|
||||
|
@ -598,10 +551,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: defaultDragging,
|
||||
},
|
||||
droppedItem: defaultDragging,
|
||||
state: testState,
|
||||
dropType: 'replace_compatible',
|
||||
|
@ -667,10 +616,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: metricDragging,
|
||||
},
|
||||
droppedItem: metricDragging,
|
||||
state: testState,
|
||||
dropType: 'duplicate_in_group',
|
||||
|
@ -703,10 +648,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
onDrop({
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging: bucketDragging,
|
||||
},
|
||||
droppedItem: bucketDragging,
|
||||
state: testState,
|
||||
dropType: 'duplicate_in_group',
|
||||
|
@ -768,10 +709,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
const defaultReorderDropParams = {
|
||||
...defaultProps,
|
||||
dragDropContext: {
|
||||
...dragDropContext,
|
||||
dragging,
|
||||
},
|
||||
dragging,
|
||||
droppedItem: dragging,
|
||||
state: testState,
|
||||
filterOperations: (op: OperationMetadata) => op.dataType === 'number',
|
||||
|
|
|
@ -23,6 +23,7 @@ import { mergeLayer } from '../state_helpers';
|
|||
import { hasField, isDraggedField } from '../utils';
|
||||
import { IndexPatternPrivateState, DraggedField } from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { DragContextState } from '../../drag_drop/providers';
|
||||
|
||||
type DropHandlerProps<T> = DatasourceDimensionDropHandlerProps<IndexPatternPrivateState> & {
|
||||
droppedItem: T;
|
||||
|
@ -31,9 +32,12 @@ type DropHandlerProps<T> = DatasourceDimensionDropHandlerProps<IndexPatternPriva
|
|||
const operationLabels = getOperationDisplay();
|
||||
|
||||
export function getDropProps(
|
||||
props: DatasourceDimensionDropProps<IndexPatternPrivateState> & { groupId: string }
|
||||
props: DatasourceDimensionDropProps<IndexPatternPrivateState> & {
|
||||
dragging: DragContextState['dragging'];
|
||||
groupId: string;
|
||||
}
|
||||
): { dropType: DropType; nextLabel?: string } | undefined {
|
||||
const { dragging } = props.dragDropContext;
|
||||
const { dragging } = props;
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -93,6 +93,21 @@ export function getIndexPatternDatasource({
|
|||
|
||||
const indexPatternsService = data.indexPatterns;
|
||||
|
||||
const handleChangeIndexPattern = (
|
||||
id: string,
|
||||
state: IndexPatternPrivateState,
|
||||
setState: StateSetter<IndexPatternPrivateState>
|
||||
) => {
|
||||
changeIndexPattern({
|
||||
id,
|
||||
state,
|
||||
setState,
|
||||
onError: onIndexPatternLoadError,
|
||||
storage,
|
||||
indexPatternsService,
|
||||
});
|
||||
};
|
||||
|
||||
// Not stateful. State is persisted to the frame
|
||||
const indexPatternDatasource: Datasource<IndexPatternPrivateState, IndexPatternPersistedState> = {
|
||||
id: 'indexpattern',
|
||||
|
@ -171,20 +186,7 @@ export function getIndexPatternDatasource({
|
|||
render(
|
||||
<I18nProvider>
|
||||
<IndexPatternDataPanel
|
||||
changeIndexPattern={(
|
||||
id: string,
|
||||
state: IndexPatternPrivateState,
|
||||
setState: StateSetter<IndexPatternPrivateState>
|
||||
) => {
|
||||
changeIndexPattern({
|
||||
id,
|
||||
state,
|
||||
setState,
|
||||
onError: onIndexPatternLoadError,
|
||||
storage,
|
||||
indexPatternsService,
|
||||
});
|
||||
}}
|
||||
changeIndexPattern={handleChangeIndexPattern}
|
||||
data={data}
|
||||
charts={charts}
|
||||
{...props}
|
||||
|
|
|
@ -253,6 +253,7 @@ export function createMockedDragDropContext(): jest.Mocked<DragContextState> {
|
|||
keyboardMode: false,
|
||||
setKeyboardMode: jest.fn(),
|
||||
setA11yMessage: jest.fn(),
|
||||
dropTargetsByOrder: undefined,
|
||||
registerDropTarget: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -190,7 +190,10 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps<T>) => void;
|
||||
renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps<T>) => void;
|
||||
getDropProps: (
|
||||
props: DatasourceDimensionDropProps<T> & { groupId: string }
|
||||
props: DatasourceDimensionDropProps<T> & {
|
||||
groupId: string;
|
||||
dragging: DragContextState['dragging'];
|
||||
}
|
||||
) => { dropType: DropType; nextLabel?: string } | undefined;
|
||||
onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => false | true | { deleted: string };
|
||||
updateStateOnCloseDimension?: (props: {
|
||||
|
@ -278,9 +281,7 @@ export type DatasourceDimensionEditorProps<T = unknown> = DatasourceDimensionPro
|
|||
dimensionGroups: VisualizationDimensionGroupConfig[];
|
||||
};
|
||||
|
||||
export type DatasourceDimensionTriggerProps<T> = DatasourceDimensionProps<T> & {
|
||||
dragDropContext: DragContextState;
|
||||
};
|
||||
export type DatasourceDimensionTriggerProps<T> = DatasourceDimensionProps<T>;
|
||||
|
||||
export interface DatasourceLayerPanelProps<T> {
|
||||
layerId: string;
|
||||
|
@ -310,7 +311,6 @@ export type DatasourceDimensionDropProps<T> = SharedDimensionProps & {
|
|||
columnId: string;
|
||||
state: T;
|
||||
setState: StateSetter<T>;
|
||||
dragDropContext: DragContextState;
|
||||
};
|
||||
|
||||
export type DatasourceDimensionDropHandlerProps<T> = DatasourceDimensionDropProps<T> & {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue