[canvas] Fix bugs in interactive workpad (#110385)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3d4b5c595d
commit
e02eb5084b
|
@ -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 !== '') {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) });
|
|
@ -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(),
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { CSSProperties, PureComponent } from 'react';
|
||||
// @ts-expect-error untyped local
|
||||
import { WORKPAD_CONTAINER_ID } from '../../workpad_app';
|
||||
|
||||
interface State {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue