[canvas] Fix bugs in interactive workpad (#110385)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2021-08-30 16:06:15 -05:00 committed by GitHub
parent 3d4b5c595d
commit e02eb5084b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 88 additions and 96 deletions

View file

@ -47,7 +47,7 @@ export const dropdownFilter: RendererFactory<Config> = () => ({
render(domNode, config, handlers) {
let filterExpression = handlers.getFilter();
if (filterExpression === undefined || filterExpression.indexOf('exactly')) {
if (filterExpression === undefined || !filterExpression.includes('exactly')) {
filterExpression = '';
handlers.setFilter(filterExpression);
} else if (filterExpression !== '') {

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export { WorkpadApp } from './workpad_app';
export { WorkpadApp, WORKPAD_CONTAINER_ID } from './workpad_app';
export { WorkpadApp as WorkpadAppComponent } from './workpad_app.component';

View file

@ -12,8 +12,8 @@ import { selectToplevelNodes } from '../../state/actions/transient';
import { arrayToMap, flatten, identity } from '../../lib/aeroelastic/functional';
import { getLocalTransformMatrix } from '../../lib/aeroelastic/layout_functions';
import { matrixToAngle } from '../../lib/aeroelastic/matrix';
import { isGroupId, elementToShape } from './utils';
export * from './utils';
import { isGroupId, elementToShape } from './positioning_utils';
export * from './positioning_utils';
const shapeToElement = (shape) => ({
left: shape.transformMatrix[12] - shape.a,

View file

@ -1,59 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { multiply, rotateZ, translate } from '../../lib/aeroelastic/matrix';
export const isGroupId = (id) => id.startsWith('group');
const headerData = (id) =>
isGroupId(id)
? { id, type: 'group', subtype: 'persistentGroup' }
: { id, type: 'rectangleElement', subtype: '' };
const transformData = ({ top, left, width, height, angle }, z) =>
multiply(
translate(left + width / 2, top + height / 2, z), // painter's algo: latest item (highest z) goes to top
rotateZ((-angle / 180) * Math.PI) // minus angle as transform:matrix3d uses a left-handed coordinate system
);
/**
* elementToShape
*
* converts a `kibana-canvas` element to an `aeroelastic` shape.
*
* Shape: the layout algorithms need to deal with objects through their geometric properties, excluding other aspects,
* such as what's inside the element, eg. image or scatter plot. This representation is, at its core, a transform matrix
* that establishes a new local coordinate system https://drafts.csswg.org/css-transforms/#local-coordinate-system plus a
* size descriptor. There are two versions of the transform matrix:
* - `transformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#current-transformation-matrix
* - `localTransformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#transformation-matrix
*
* Element: it also needs to represent the geometry, primarily because of the need to persist it in `redux` and on the
* server, and to accept such data from the server. The redux and server representations will need to change as more general
* projections such as 3D are added. The element also needs to maintain its content, such as an image or a plot.
*
* While all elements on the current page also exist as shapes, there are shapes that are not elements: annotations.
* For example, `rotation_handle`, `border_resize_handle` and `border_connection` are modeled as shapes by the layout
* library, simply for generality.
*/
export const elementToShape = ({ id, position }, z) => ({
...headerData(id),
parent: (position && position.parent) || null,
transformMatrix: transformData(position, z),
a: position.width / 2, // we currently specify half-width, half-height as it leads to
b: position.height / 2, // more regular math (like ellipsis radii rather than diameters)
});
const simplePosition = ({ id, position, filter }, z) => ({
...headerData(id),
width: position.width,
height: position.height,
transformMatrix: transformData(position, z),
filter,
});
export const simplePositioning = ({ elements }) => ({ elements: elements.map(simplePosition) });

View file

@ -5,7 +5,27 @@
* 2.0.
*/
const localMousePosition = (canvasOrigin, clientX, clientY, zoomScale = 1) => {
import { CommitFn } from '../../../lib/aeroelastic';
import { WORKPAD_CONTAINER_ID } from '../../workpad_app/workpad_app.component';
type CanvasOriginFn = () => { left: number; top: number };
export interface Props {
commit: CommitFn | undefined;
canvasOrigin: CanvasOriginFn;
zoomScale: number;
canDragElement: (target: EventTarget | null) => boolean;
}
const isInCanvas = (target: EventTarget | null) =>
target instanceof Element && target.closest(`#${WORKPAD_CONTAINER_ID}`);
const localMousePosition = (
canvasOrigin: CanvasOriginFn,
clientX: number,
clientY: number,
zoomScale = 1
) => {
const { left, top } = canvasOrigin();
return {
// commit unscaled coordinates
@ -19,12 +39,26 @@ const resetHandler = () => {
window.onmouseup = null;
};
const setupHandler = (commit, canvasOrigin, zoomScale) => {
const setupHandler = (commit: CommitFn, canvasOrigin: CanvasOriginFn, zoomScale?: number) => {
// Ancestor has to be identified on setup, rather than 1st interaction, otherwise events may be triggered on
// DOM elements that had been removed: kibana-canvas github issue #1093
window.onmousemove = ({ buttons, clientX, clientY, altKey, metaKey, shiftKey, ctrlKey }) => {
window.onmousemove = ({
buttons,
clientX,
clientY,
altKey,
metaKey,
shiftKey,
ctrlKey,
target,
}: MouseEvent) => {
if (!isInCanvas(target)) {
return;
}
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale);
// only commits the cursor position if there's a way to latch onto x/y calculation (canvasOrigin is knowable)
// or if left button is being held down (i.e. an element is being dragged)
if (buttons === 1 || canvasOrigin) {
@ -34,9 +68,15 @@ const setupHandler = (commit, canvasOrigin, zoomScale) => {
commit('cursorPosition', {});
}
};
window.onmouseup = (e) => {
window.onmouseup = (e: MouseEvent) => {
const { clientX, clientY, altKey, metaKey, shiftKey, ctrlKey, target } = e;
if (!isInCanvas(target)) {
return;
}
e.stopPropagation();
const { clientX, clientY, altKey, metaKey, shiftKey, ctrlKey } = e;
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale);
commit('mouseEvent', { event: 'mouseUp', x, y, altKey, metaKey, shiftKey, ctrlKey });
resetHandler();
@ -44,26 +84,46 @@ const setupHandler = (commit, canvasOrigin, zoomScale) => {
};
const handleMouseMove = (
commit,
{ clientX, clientY, altKey, metaKey, shiftKey, ctrlKey },
canvasOrigin,
zoomScale
commit: CommitFn | undefined,
{ clientX, clientY, altKey, metaKey, shiftKey, ctrlKey, target }: MouseEvent,
canvasOrigin: CanvasOriginFn,
zoomScale?: number
) => {
if (!isInCanvas(target)) {
return;
}
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale);
if (commit) {
commit('cursorPosition', { x, y, altKey, metaKey, shiftKey, ctrlKey });
}
};
const handleMouseLeave = (commit, { buttons }) => {
const handleMouseLeave = (commit: CommitFn | undefined, { buttons, target }: MouseEvent) => {
if (!isInCanvas(target)) {
return;
}
if (buttons !== 1 && commit) {
commit('cursorPosition', {}); // reset hover only if we're not holding down left key (ie. drag in progress)
}
};
const handleMouseDown = (commit, e, canvasOrigin, zoomScale, allowDrag = true) => {
const handleMouseDown = (
commit: CommitFn | undefined,
e: MouseEvent,
canvasOrigin: CanvasOriginFn,
zoomScale: number,
allowDrag = true
) => {
const { clientX, clientY, buttons, altKey, metaKey, shiftKey, ctrlKey, target } = e;
if (!isInCanvas(target)) {
return;
}
e.stopPropagation();
const { clientX, clientY, buttons, altKey, metaKey, shiftKey, ctrlKey } = e;
if (buttons !== 1 || !commit) {
resetHandler();
return; // left-click only
@ -82,7 +142,7 @@ const handleMouseDown = (commit, e, canvasOrigin, zoomScale, allowDrag = true) =
};
export const eventHandlers = {
onMouseDown: (props) => (e) =>
onMouseDown: (props: Props) => (e: MouseEvent) =>
handleMouseDown(
props.commit,
e,
@ -90,9 +150,10 @@ export const eventHandlers = {
props.zoomScale,
props.canDragElement(e.target)
),
onMouseMove: (props) => (e) =>
onMouseMove: (props: Props) => (e: MouseEvent) =>
handleMouseMove(props.commit, e, props.canvasOrigin, props.zoomScale),
onMouseLeave: (props) => (e) => handleMouseLeave(props.commit, e),
onWheel: (props) => (e) => handleMouseMove(props.commit, e, props.canvasOrigin),
onMouseLeave: (props: Props) => (e: MouseEvent) => handleMouseLeave(props.commit, e),
onWheel: (props: Props) => (e: WheelEvent) =>
handleMouseMove(props.commit, e, props.canvasOrigin),
resetHandler: () => () => resetHandler(),
};

View file

@ -243,17 +243,7 @@ export const InteractivePage = compose(
})),
withProps((...props) => ({
...props,
canDragElement: (element) => {
return !isEmbeddableBody(element) && isInWorkpad(element);
const hasClosest = typeof element.closest === 'function';
if (hasClosest) {
return !element.closest('.embeddable') || element.closest('.embPanel__header');
} else {
return !closest.call(element, '.embeddable') || closest.call(element, '.embPanel__header');
}
},
canDragElement: (element) => !isEmbeddableBody(element) && isInWorkpad(element),
})),
withHandlers(eventHandlers), // Captures user intent, needs to have reconciled state
() => InteractiveComponent

View file

@ -6,7 +6,6 @@
*/
import React, { CSSProperties, PureComponent } from 'react';
// @ts-expect-error untyped local
import { WORKPAD_CONTAINER_ID } from '../../workpad_app';
interface State {

View file

@ -44,6 +44,8 @@ export type TypeName = string;
export type Payload = JsonMap;
export type UpdaterFunction = (arg: State) => State;
export type CommitFn = (type: TypeName, payload: Payload) => void;
export interface Store {
getCurrentState: () => State;
setCurrentState: (state: State) => void;

View file

@ -819,7 +819,7 @@ const resizePointAnnotations = (config, parent, a, b) => ([x, y, cursorAngle]) =
const xName = xNames[x];
const yName = yNames[y];
return {
id: [config.resizeHandleName, xName, yName, parent].join('_'),
id: [config.resizeHandleName, xName, yName, parent.id].join('_'),
type: 'annotation',
subtype: config.resizeHandleName,
horizontalPosition: xName,

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { ActionId, Payload, State, Store, TypeName, UpdaterFunction } from '.';
import { ActionId, CommitFn, State, Store, UpdaterFunction } from '.';
let counter = 0 as ActionId;
export const createStore = (initialState: State, updater: UpdaterFunction): Store => {
let currentState = initialState;
const commit = (type: TypeName, payload: Payload) => {
const commit: CommitFn = (type, payload) => {
return (currentState = updater({
...currentState,
primaryUpdate: {

View file

@ -10,8 +10,7 @@ import React, { FC, PureComponent } from 'react';
import Style from 'style-it';
import { AnyExpressionFunctionDefinition } from '../../../../../src/plugins/expressions';
import { Positionable } from '../../public/components/positionable/positionable';
// @ts-expect-error untyped local
import { elementToShape } from '../../public/components/workpad_page/utils';
import { elementToShape } from '../../public/components/workpad_page/positioning_utils';
import { CanvasRenderedElement } from '../types';
import { CanvasShareableContext, useCanvasShareableState } from '../context';
import { AnyRendererSpec } from '../../types';