[Lens] Drag and drop performance improvements (#91641) (#92122)

This commit is contained in:
Marta Bondyra 2021-02-21 12:15:06 +01:00 committed by GitHub
parent d9828c22d5
commit 90fc153d85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 698 additions and 746 deletions

View file

@ -45,6 +45,9 @@
.lnsDragDrop-isDropTarget {
@include lnsDroppable;
@include lnsDroppableActive;
> * {
pointer-events: none;
}
}
.lnsDragDrop-isActiveGroup {

View file

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

View file

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

View file

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

View 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';

View file

@ -7,7 +7,7 @@
import React, { useContext } from 'react';
import { mount } from 'enzyme';
import { RootDragDropProvider, DragContext } from './providers';
import { RootDragDropProvider, DragContext } from '.';
jest.useFakeTimers();

View file

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

View file

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

View 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;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -794,6 +794,7 @@ describe('workspace_panel', () => {
setKeyboardMode={() => {}}
setA11yMessage={() => {}}
registerDropTarget={jest.fn()}
dropTargetsByOrder={undefined}
>
<WorkspacePanel
activeDatasourceId={'mock'}

View file

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

View file

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

View file

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

View file

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

View file

@ -253,6 +253,7 @@ export function createMockedDragDropContext(): jest.Mocked<DragContextState> {
keyboardMode: false,
setKeyboardMode: jest.fn(),
setA11yMessage: jest.fn(),
dropTargetsByOrder: undefined,
registerDropTarget: jest.fn(),
};
}

View file

@ -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> & {