diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index 814efc50bfb9786..a303b3418c040f3 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -38,7 +38,7 @@ export class ResolverEmbeddable extends Embeddable { } this.lastRenderTarget = node; // TODO, figure out how to destroy middleware - const { store } = storeFactory({ httpServiceBase: this.httpService }); + const { store } = storeFactory({ httpService: this.httpService }); ReactDOM.render(, node); } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts index 5a4f33f3fbf7e72..7925aea479a5fa8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -5,9 +5,13 @@ */ import { i18n } from '@kbn/i18n'; -import { EmbeddableFactory, EmbeddableInput, IContainer } from 'src/plugins/embeddable/public'; import { HttpSetup } from 'kibana/public'; -import { ResolverEmbeddable } from './'; +import { + EmbeddableFactory, + IContainer, + EmbeddableInput, +} from '../../../../../../src/plugins/embeddable/public'; +import { ResolverEmbeddable } from './embeddable'; export class ResolverEmbeddableFactory extends EmbeddableFactory { public readonly type = 'resolver'; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts index 3251e624484bf9e..58b72b911355c62 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts @@ -44,21 +44,13 @@ interface UserSetPanningOffset { interface UserStartedPanning { readonly type: 'userStartedPanning'; /** - * A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen.) + * A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen) + * relative to the Resolver component. * Represents a starting position during panning for a pointing device. */ readonly payload: Vector2; } -interface UserContinuedPanning { - readonly type: 'userContinuedPanning'; - /** - * A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen.) - * Represents the current position during panning for a pointing device. - */ - readonly payload: Vector2; -} - interface UserStoppedPanning { readonly type: 'userStoppedPanning'; } @@ -67,12 +59,11 @@ interface UserCanceledPanning { readonly type: 'userCanceledPanning'; } -// This action is blacklisted in redux dev tools -interface UserFocusedOnWorldCoordinates { - readonly type: 'userFocusedOnWorldCoordinates'; +interface UserMovedPointer { + readonly type: 'userMovedPointer'; /** - * World coordinates indicating a point that the user's pointing device is hoving over. - * When the camera's scale is changed, we make sure to adjust its tranform so that the these world coordinates are in the same place on the screen + * A vector in screen coordinates relative to the Resolver component. + * The payload should be contain clientX and clientY minus the client position of the Resolver component. */ readonly payload: Vector2; } @@ -82,8 +73,7 @@ export type CameraAction = | UserSetRasterSize | UserSetPanningOffset | UserStartedPanning - | UserContinuedPanning | UserStoppedPanning | UserCanceledPanning - | UserFocusedOnWorldCoordinates - | UserZoomed; + | UserZoomed + | UserMovedPointer; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts index d7d6a01d64b2d3a..361b2f59620dda6 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts @@ -40,7 +40,7 @@ describe('panning interaction', () => { }); describe('when the user continues to pan 50px up and to the right', () => { beforeEach(() => { - const action: CameraAction = { type: 'userContinuedPanning', payload: [150, 50] }; + const action: CameraAction = { type: 'userMovedPointer', payload: [150, 50] }; store.dispatch(action); }); it('should have a translation of 50,50', () => { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts index 467ba0efea5195f..fb2f137ee77abb2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts @@ -73,19 +73,6 @@ export const cameraReducer: Reducer = ( currentOffset: action.payload, }, }; - } else if (action.type === 'userContinuedPanning') { - if (userIsPanning(state)) { - return { - // This logic means, if the user calls `userContinuedPanning` without starting panning first, we start automatically basically? - ...state, - panning: { - origin: state.panning ? state.panning.origin : action.payload, - currentOffset: action.payload, - }, - }; - } else { - return state; - } } else if (action.type === 'userStoppedPanning') { if (userIsPanning(state)) { return { @@ -106,10 +93,25 @@ export const cameraReducer: Reducer = ( ...state, rasterSize: action.payload, }; - } else if (action.type === 'userFocusedOnWorldCoordinates') { + } else if (action.type === 'userMovedPointer') { return { ...state, - latestFocusedWorldCoordinates: action.payload, + /** + * keep track of the last world coordinates the user moved over. + * When the scale of the projection matrix changes, we adjust the camera's world transform in order + * to keep the same point under the pointer. + * In order to do this, we need to know the position of the mouse when changing the scale. + */ + latestFocusedWorldCoordinates: applyMatrix3(action.payload, inverseProjectionMatrix(state)), + /** + * If the user is panning, adjust the panning offset + */ + panning: userIsPanning(state) + ? { + origin: state.panning ? state.panning.origin : action.payload, + currentOffset: action.payload, + } + : state.panning, }; } else { return state; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts index bdcd6dfd07c39f3..3429ce78cde1d31 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts @@ -75,9 +75,10 @@ describe('zooming', () => { }); describe('when the user has moved their mouse to the raster position 200, 50', () => { beforeEach(() => { + // TODO update action const action: CameraAction = { - type: 'userFocusedOnWorldCoordinates', - payload: applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), + type: 'userMovedPointer', + payload: [200, 50], }; store.dispatch(action); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts index 7a435b8b1e3bb4b..3b3f51e9821c5c6 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -5,10 +5,11 @@ */ import { createStore, StoreEnhancer } from 'redux'; +import { ResolverAction } from '../types'; +import { HttpSetup } from '../../../../../../../src/core/public'; import { resolverReducer } from './reducer'; -import { HttpServiceBase } from '../../../../../../../src/core/public'; -export const storeFactory = ({ httpServiceBase }: { httpServiceBase: HttpServiceBase }) => { +export const storeFactory = (_dependencies: { httpService: HttpSetup }) => { interface SomethingThatMightHaveReduxDevTools { __REDUX_DEVTOOLS_EXTENSION__?: (options?: { name?: string; @@ -16,12 +17,14 @@ export const storeFactory = ({ httpServiceBase }: { httpServiceBase: HttpService }) => StoreEnhancer; } const windowWhichMightHaveReduxDevTools = window as SomethingThatMightHaveReduxDevTools; + // Make sure blacklisted action types are valid + const actionsBlacklist: ReadonlyArray = ['userMovedPointer']; const store = createStore( resolverReducer, windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__ && windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__({ name: 'Resolver', - actionsBlacklist: ['userFocusedOnWorldCoordinates'], + actionsBlacklist, }) ); return { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 19744cf41ed7791..67812a5eebc6d8d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -30,22 +30,17 @@ const Diagnostic = styled( const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect(); - const inverseProjectionMatrix = useSelector(selectors.inverseProjectionMatrix); - - const worldPositionFromClientPosition = useCallback( - (clientPosition: Vector2): Vector2 | null => { + const relativeCoordinatesFromMouseEvent = useCallback( + (event: { clientX: number; clientY: number }): null | [number, number] => { if (elementBoundingClientRect === undefined) { return null; } - return applyMatrix3( - [ - clientPosition[0] - elementBoundingClientRect.x, - clientPosition[1] - elementBoundingClientRect.y, - ], - inverseProjectionMatrix - ); + return [ + event.clientX - elementBoundingClientRect.x, + event.clientY - elementBoundingClientRect.y, + ]; }, - [inverseProjectionMatrix, elementBoundingClientRect] + [elementBoundingClientRect] ); useEffect(() => { @@ -59,35 +54,28 @@ const Diagnostic = styled( const handleMouseDown = useCallback( (event: React.MouseEvent) => { - dispatch({ - type: 'userStartedPanning', - payload: [event.clientX, event.clientY], - }); + const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); + if (maybeCoordinates !== null) { + dispatch({ + type: 'userStartedPanning', + payload: maybeCoordinates, + }); + } }, - [dispatch] + [dispatch, relativeCoordinatesFromMouseEvent] ); const handleMouseMove = useCallback( (event: MouseEvent) => { - if (event.buttons === 1 && userIsPanning) { + const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); + if (maybeCoordinates) { dispatch({ - type: 'userContinuedPanning', - payload: [event.clientX, event.clientY], - }); - } - // TODO, don't fire two actions here. make userContinuedPanning also pass world position - const maybeClientWorldPosition = worldPositionFromClientPosition([ - event.clientX, - event.clientY, - ]); - if (maybeClientWorldPosition !== null) { - dispatch({ - type: 'userFocusedOnWorldCoordinates', - payload: maybeClientWorldPosition, + type: 'userMovedPointer', + payload: maybeCoordinates, }); } }, - [dispatch, userIsPanning, worldPositionFromClientPosition] + [dispatch, relativeCoordinatesFromMouseEvent] ); const handleMouseUp = useCallback(() => { @@ -131,8 +119,6 @@ const Diagnostic = styled( }; }, [handleMouseMove]); - // TODO, handle mouse up when no longer on element or event window. ty - const dotPositions = useMemo( (): ReadonlyArray => [ [0, 0], @@ -156,16 +142,7 @@ const Diagnostic = styled( [clientRectCallback] ); - useEffect(() => { - // Set the 'wheel' event listener directly on the element - // React sets event listeners on the window and routes them back via event propagation. As of Chrome 73 or something, 'wheel' events on the 'window' are automatically treated as 'passive'. Seems weird, but whatever - if (ref !== null) { - ref.addEventListener('wheel', handleWheel); - return () => { - ref.removeEventListener('wheel', handleWheel); - }; - } - }, [handleWheel, ref]); + useNonPassiveWheelHandler(handleWheel, ref); return (
@@ -182,6 +159,36 @@ const Diagnostic = styled( position: relative; `; +const DiagnosticDot = styled( + React.memo(({ className, worldPosition }: { className?: string; worldPosition: Vector2 }) => { + const projectionMatrix = useSelector(selectors.projectionMatrix); + const [left, top] = applyMatrix3(worldPosition, projectionMatrix); + const style = { + left: (left - 20).toString() + 'px', + top: (top - 20).toString() + 'px', + }; + return ( + + x: {worldPosition[0]} +
+ y: {worldPosition[1]} +
+ ); + }) +)` + position: absolute; + width: 40px; + height: 40px; + text-align: left; + font-size: 10px; + user-select: none; + border: 1px solid black; + box-sizing: border-box; + border-radius: 10%; + padding: 4px; + white-space: nowrap; +`; + /** * Returns a DOMRect sometimes, and a `ref` callback. Put the `ref` as the `ref` property of an element, and * DOMRect will be the result of getBoundingClientRect on it. @@ -215,32 +222,22 @@ function useAutoUpdatingClientRect(): [DOMRect | undefined, (node: Element | nul return [rect, ref]; } -const DiagnosticDot = styled( - React.memo(({ className, worldPosition }: { className?: string; worldPosition: Vector2 }) => { - const projectionMatrix = useSelector(selectors.projectionMatrix); - const [left, top] = applyMatrix3(worldPosition, projectionMatrix); - const style = { - left: (left - 20).toString() + 'px', - top: (top - 20).toString() + 'px', - }; - return ( - - x: {worldPosition[0]} -
- y: {worldPosition[1]} -
- ); - }) -)` - position: absolute; - width: 40px; - height: 40px; - text-align: left; - font-size: 10px; - user-select: none; - border: 1px solid black; - box-sizing: border-box; - border-radius: 10%; - padding: 4px; - white-space: nowrap; -`; +/** + * Register an event handler directly on `elementRef` for the `wheel` event, with no options + * React sets native event listeners on the `window` and calls provided handlers via event propagation. + * As of Chrome 73, `'wheel'` events on `window` are automatically treated as 'passive'. + * If you don't need to call `event.preventDefault` then you should use regular React event handling instead. + */ +function useNonPassiveWheelHandler( + handler: (event: WheelEvent) => void, + elementRef: HTMLElement | null +) { + useEffect(() => { + if (elementRef !== null) { + elementRef.addEventListener('wheel', handler); + return () => { + elementRef.removeEventListener('wheel', handler); + }; + } + }, [elementRef, handler]); +}